use cdk_common::dhke::construct_proofs;
use cdk_common::nuts::{Conditions, SigFlag, SpendingConditions};
use cdk_common::Amount;
use crate::test_helpers::mint::create_test_blinded_messages;
use crate::test_helpers::nut10::{create_test_keypair, unzip3, TestMintHelper};
use crate::util::unix_time;
#[tokio::test]
async fn test_p2pk_single_pubkey_requires_all_proofs_signed() {
let test_mint = TestMintHelper::new().await.unwrap();
let mint = test_mint.mint();
let (alice_secret, alice_pubkey) = create_test_keypair();
println!("Alice pubkey: {}", alice_pubkey);
let input_amount = Amount::from(10);
let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
let spending_conditions = SpendingConditions::new_p2pk(alice_pubkey, None);
let split_amounts = test_mint.split_amount(input_amount).unwrap();
let split_display: Vec<String> = split_amounts.iter().map(|a| a.to_string()).collect();
println!("Split {} into [{}]", input_amount, split_display.join("+"));
let (p2pk_outputs, blinding_factors, secrets) = unzip3(
split_amounts
.iter()
.map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
.collect(),
);
println!(
"Created {} P2PK outputs locked to alice",
p2pk_outputs.len()
);
let swap_request =
cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
let swap_response = mint
.process_swap_request(swap_request)
.await
.expect("Failed to swap for P2PK proofs");
println!("Swap successful! Got BlindSignatures for our P2PK outputs");
let p2pk_proofs = construct_proofs(
swap_response.signatures.clone(),
blinding_factors.clone(),
secrets.clone(),
&test_mint.public_keys_of_the_active_sat_keyset,
)
.unwrap();
let proof_amounts: Vec<String> = p2pk_proofs.iter().map(|p| p.amount.to_string()).collect();
println!(
"Constructed {} P2PK proof(s) [{}]",
p2pk_proofs.len(),
proof_amounts.join("+")
);
let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
.await
.unwrap();
let swap_request_no_sig =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
let result = mint.process_swap_request(swap_request_no_sig).await;
assert!(result.is_err(), "Should fail without signature");
println!("✓ Spending WITHOUT signature failed as expected");
let mut swap_request_partial_sig =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
swap_request_partial_sig.inputs_mut()[0]
.sign_p2pk(alice_secret.clone())
.unwrap();
let result = mint.process_swap_request(swap_request_partial_sig).await;
assert!(result.is_err(), "Should fail with only partial signatures");
println!("✓ Spending with PARTIAL signatures failed as expected");
let mut swap_request_with_sig =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_with_sig.inputs_mut() {
proof.sign_p2pk(alice_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_with_sig).await;
assert!(result.is_ok(), "Should succeed with valid signature");
println!("✓ Spending WITH ALL signatures succeeded");
}
#[tokio::test]
async fn test_p2pk_multisig_2of3() {
let test_mint = TestMintHelper::new().await.unwrap();
let mint = test_mint.mint();
let (alice_secret, alice_pubkey) = create_test_keypair();
let (bob_secret, bob_pubkey) = create_test_keypair();
let (_carol_secret, carol_pubkey) = create_test_keypair();
let (dave_secret, _dave_pubkey) = create_test_keypair();
let (eve_secret, _eve_pubkey) = create_test_keypair();
println!("Alice: {}", alice_pubkey);
println!("Bob: {}", bob_pubkey);
println!("Carol: {}", carol_pubkey);
let input_amount = Amount::from(10);
let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
let spending_conditions = SpendingConditions::new_p2pk(
alice_pubkey,
Some(
Conditions::new(
None, Some(vec![bob_pubkey, carol_pubkey]), None, Some(2), None, None, )
.unwrap(),
),
);
println!("Created 2-of-3 multisig spending conditions (Alice, Bob, Carol)");
let split_amounts = test_mint.split_amount(input_amount).unwrap();
let (p2pk_outputs, blinding_factors, secrets) = unzip3(
split_amounts
.iter()
.map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
.collect(),
);
let swap_request =
cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
let swap_response = mint.process_swap_request(swap_request).await.unwrap();
println!("Created P2PK multisig proofs (2-of-3)");
let p2pk_proofs = construct_proofs(
swap_response.signatures.clone(),
blinding_factors.clone(),
secrets.clone(),
&test_mint.public_keys_of_the_active_sat_keyset,
)
.unwrap();
let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
.await
.unwrap();
let mut swap_request_one_sig =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_one_sig.inputs_mut() {
proof.sign_p2pk(alice_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_one_sig).await;
assert!(
result.is_err(),
"Should fail with only 1 signature (need 2)"
);
println!("✓ Spending with only 1 signature (Alice) failed as expected");
let mut swap_request_invalid_sigs =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_invalid_sigs.inputs_mut() {
proof.sign_p2pk(dave_secret.clone()).unwrap();
proof.sign_p2pk(eve_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_invalid_sigs).await;
assert!(result.is_err(), "Should fail with 2 invalid signatures");
println!("✓ Spending with 2 INVALID signatures (Dave + Eve) failed as expected");
let mut swap_request_valid_sigs =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_valid_sigs.inputs_mut() {
proof.sign_p2pk(alice_secret.clone()).unwrap();
proof.sign_p2pk(bob_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_valid_sigs).await;
assert!(result.is_ok(), "Should succeed with 2 valid signatures");
println!("✓ Spending with 2 VALID signatures (Alice + Bob) succeeded");
}
#[tokio::test]
async fn test_p2pk_locktime_before_expiry() {
let test_mint = TestMintHelper::new().await.unwrap();
let mint = test_mint.mint();
let (alice_secret, alice_pubkey) = create_test_keypair();
let (bob_secret, bob_pubkey) = create_test_keypair();
let locktime = unix_time() + 3600;
println!("Alice (primary): {}", alice_pubkey);
println!("Bob (refund): {}", bob_pubkey);
println!("Current time: {}", unix_time());
println!("Locktime: {} (expires in 1 hour)", locktime);
let input_amount = Amount::from(10);
let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
let spending_conditions = SpendingConditions::new_p2pk(
alice_pubkey,
Some(
Conditions::new(
Some(locktime), None, Some(vec![bob_pubkey]), None, None, None, )
.unwrap(),
),
);
println!("Created P2PK with locktime and refund key");
let split_amounts = test_mint.split_amount(input_amount).unwrap();
let (p2pk_outputs, blinding_factors, secrets) = unzip3(
split_amounts
.iter()
.map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
.collect(),
);
let swap_request =
cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
let swap_response = mint.process_swap_request(swap_request).await.unwrap();
let p2pk_proofs = construct_proofs(
swap_response.signatures.clone(),
blinding_factors.clone(),
secrets.clone(),
&test_mint.public_keys_of_the_active_sat_keyset,
)
.unwrap();
let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
.await
.unwrap();
let mut swap_request_refund =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_refund.inputs_mut() {
proof.sign_p2pk(bob_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_refund).await;
assert!(
result.is_err(),
"Should fail - refund key cannot spend before locktime"
);
println!("✓ Spending with refund key (Bob) BEFORE locktime failed as expected");
let mut swap_request_primary =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_primary.inputs_mut() {
proof.sign_p2pk(alice_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_primary).await;
assert!(
result.is_ok(),
"Should succeed - primary key can spend before locktime"
);
println!("✓ Spending with primary key (Alice) BEFORE locktime succeeded");
}
#[tokio::test]
async fn test_p2pk_locktime_after_expiry() {
let test_mint = TestMintHelper::new().await.unwrap();
let mint = test_mint.mint();
let (alice_secret, alice_pubkey) = create_test_keypair();
let (_bob_secret, bob_pubkey) = create_test_keypair();
let locktime = unix_time() - 3600;
println!("Alice (primary): {}", alice_pubkey);
println!("Bob (refund): {}", bob_pubkey);
println!("Current time: {}", unix_time());
println!("Locktime: {} (expired 1 hour ago)", locktime);
let input_amount = Amount::from(10);
let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
let spending_conditions = SpendingConditions::new_p2pk(
alice_pubkey,
Some(Conditions {
locktime: Some(locktime), pubkeys: None, refund_keys: Some(vec![bob_pubkey]), num_sigs: None, sig_flag: SigFlag::default(),
num_sigs_refund: None, }),
);
println!("Created P2PK with expired locktime and refund key");
let split_amounts = test_mint.split_amount(input_amount).unwrap();
let (p2pk_outputs, blinding_factors, secrets) = unzip3(
split_amounts
.iter()
.map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
.collect(),
);
let swap_request =
cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
let swap_response = mint.process_swap_request(swap_request).await.unwrap();
let p2pk_proofs = construct_proofs(
swap_response.signatures.clone(),
blinding_factors.clone(),
secrets.clone(),
&test_mint.public_keys_of_the_active_sat_keyset,
)
.unwrap();
let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
.await
.unwrap();
let mut swap_request_primary =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_primary.inputs_mut() {
proof.sign_p2pk(alice_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_primary).await;
assert!(
result.is_ok(),
"Should succeed - primary key can STILL spend after locktime (NUT-11 compliant): {:?}",
result.err()
);
println!("✓ Spending with primary key (Alice) AFTER locktime succeeded (NUT-11 compliant)");
}
#[tokio::test]
async fn test_p2pk_locktime_after_expiry_no_refund_anyone_can_spend() {
let test_mint = TestMintHelper::new().await.unwrap();
let mint = test_mint.mint();
let (_alice_secret, alice_pubkey) = create_test_keypair();
let locktime = unix_time() - 3600;
println!("Alice (primary): {}", alice_pubkey);
println!("Current time: {}", unix_time());
println!("Locktime: {} (expired 1 hour ago)", locktime);
println!("No refund keys configured - anyone can spend after locktime");
let input_amount = Amount::from(10);
let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
let spending_conditions = SpendingConditions::new_p2pk(
alice_pubkey,
Some(Conditions {
locktime: Some(locktime), pubkeys: None, refund_keys: None, num_sigs: None, sig_flag: SigFlag::default(),
num_sigs_refund: None, }),
);
println!("Created P2PK with expired locktime and NO refund keys");
let split_amounts = test_mint.split_amount(input_amount).unwrap();
let (p2pk_outputs, blinding_factors, secrets) = unzip3(
split_amounts
.iter()
.map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
.collect(),
);
let swap_request =
cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
let swap_response = mint.process_swap_request(swap_request).await.unwrap();
let p2pk_proofs = construct_proofs(
swap_response.signatures.clone(),
blinding_factors.clone(),
secrets.clone(),
&test_mint.public_keys_of_the_active_sat_keyset,
)
.unwrap();
let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
.await
.unwrap();
let swap_request_no_sig =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
let result = mint.process_swap_request(swap_request_no_sig).await;
assert!(
result.is_ok(),
"Should succeed - anyone can spend after locktime with no refund keys: {:?}",
result.err()
);
println!("✓ Spending WITHOUT any signatures succeeded (anyone can spend)");
}
#[tokio::test]
async fn test_p2pk_multisig_locktime() {
let test_mint = TestMintHelper::new().await.unwrap();
let mint = test_mint.mint();
let (alice_secret, alice_pubkey) = create_test_keypair();
let (bob_secret, bob_pubkey) = create_test_keypair();
let (_carol_secret, carol_pubkey) = create_test_keypair();
let (_dave_secret, dave_pubkey) = create_test_keypair();
let (_eve_secret, eve_pubkey) = create_test_keypair();
let locktime = unix_time() - 100;
println!("Primary multisig: Alice, Bob, Carol (need 2-of-3)");
println!("Refund multisig: Dave, Eve (need 1-of-2)");
println!("Current time: {}", unix_time());
println!("Locktime: {} (expired)", locktime);
let input_amount = Amount::from(10);
let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
let spending_conditions = SpendingConditions::new_p2pk(
alice_pubkey,
Some(Conditions {
locktime: Some(locktime), pubkeys: Some(vec![bob_pubkey, carol_pubkey]), refund_keys: Some(vec![dave_pubkey, eve_pubkey]), num_sigs: Some(2), sig_flag: SigFlag::default(),
num_sigs_refund: Some(1), }),
);
println!("Created complex P2PK: 2-of-3 before locktime, 1-of-2 after locktime");
let split_amounts = test_mint.split_amount(input_amount).unwrap();
let (p2pk_outputs, blinding_factors, secrets) = unzip3(
split_amounts
.iter()
.map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
.collect(),
);
let swap_request =
cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
let swap_response = mint.process_swap_request(swap_request).await.unwrap();
let p2pk_proofs = construct_proofs(
swap_response.signatures.clone(),
blinding_factors.clone(),
secrets.clone(),
&test_mint.public_keys_of_the_active_sat_keyset,
)
.unwrap();
let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
.await
.unwrap();
let mut swap_request_primary =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_primary.inputs_mut() {
proof.sign_p2pk(alice_secret.clone()).unwrap();
proof.sign_p2pk(bob_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_primary).await;
assert!(
result.is_ok(),
"Should succeed - primary keys (2-of-3) can STILL spend after locktime (NUT-11): {:?}",
result.err()
);
println!(
"✓ Spending with primary keys (Alice + Bob, 2-of-3) AFTER locktime succeeded (NUT-11)"
);
}
#[tokio::test]
async fn test_p2pk_signed_by_wrong_person() {
let test_mint = TestMintHelper::new().await.unwrap();
let mint = test_mint.mint();
let (_alice_secret, alice_pubkey) = create_test_keypair();
let (bob_secret, _bob_pubkey) = create_test_keypair();
println!("Alice pubkey: {}", alice_pubkey);
println!("Bob will try to spend Alice's proofs");
let input_amount = Amount::from(10);
let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
let spending_conditions = SpendingConditions::new_p2pk(alice_pubkey, None);
let split_amounts = test_mint.split_amount(input_amount).unwrap();
let (p2pk_outputs, blinding_factors, secrets) = unzip3(
split_amounts
.iter()
.map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
.collect(),
);
let swap_request =
cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
let swap_response = mint.process_swap_request(swap_request).await.unwrap();
println!("Created P2PK proofs locked to Alice");
let p2pk_proofs = construct_proofs(
swap_response.signatures.clone(),
blinding_factors.clone(),
secrets.clone(),
&test_mint.public_keys_of_the_active_sat_keyset,
)
.unwrap();
let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
.await
.unwrap();
let mut swap_request_wrong_sig =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_wrong_sig.inputs_mut() {
proof.sign_p2pk(bob_secret.clone()).unwrap();
}
let result = mint.process_swap_request(swap_request_wrong_sig).await;
assert!(result.is_err(), "Should fail when signed with wrong key");
println!("✓ Spending signed by wrong person failed as expected");
}
#[tokio::test]
async fn test_p2pk_duplicate_signatures() {
let test_mint = TestMintHelper::new().await.unwrap();
let mint = test_mint.mint();
let (alice_secret, alice_pubkey) = create_test_keypair();
let (_bob_secret, bob_pubkey) = create_test_keypair();
println!("Alice: {}", alice_pubkey);
println!("Bob: {}", bob_pubkey);
let input_amount = Amount::from(10);
let input_proofs = test_mint.mint_proofs(input_amount).await.unwrap();
let spending_conditions = SpendingConditions::new_p2pk(
alice_pubkey,
Some(
Conditions::new(
None, Some(vec![bob_pubkey]), None, Some(2), None, None, )
.unwrap(),
),
);
println!("Created 2-of-2 multisig (Alice, Bob)");
let split_amounts = test_mint.split_amount(input_amount).unwrap();
let (p2pk_outputs, blinding_factors, secrets) = unzip3(
split_amounts
.iter()
.map(|&amt| test_mint.create_blinded_message(amt, &spending_conditions))
.collect(),
);
let swap_request =
cdk_common::nuts::SwapRequest::new(input_proofs.clone(), p2pk_outputs.clone());
let swap_response = mint.process_swap_request(swap_request).await.unwrap();
let p2pk_proofs = construct_proofs(
swap_response.signatures.clone(),
blinding_factors.clone(),
secrets.clone(),
&test_mint.public_keys_of_the_active_sat_keyset,
)
.unwrap();
let (new_outputs, _) = create_test_blinded_messages(mint, input_amount)
.await
.unwrap();
let mut swap_request_duplicate =
cdk_common::nuts::SwapRequest::new(p2pk_proofs.clone(), new_outputs.clone());
for proof in swap_request_duplicate.inputs_mut() {
proof.sign_p2pk(alice_secret.clone()).unwrap();
proof.sign_p2pk(alice_secret.clone()).unwrap(); }
let result = mint.process_swap_request(swap_request_duplicate).await;
assert!(
result.is_err(),
"Should fail - duplicate signatures not allowed"
);
println!("✓ Spending with duplicate signatures (Alice + Alice) failed as expected");
}