From 755b49654efd1fe694b9fbd7b64941241309a756 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 7 May 2026 00:54:01 -0300 Subject: [PATCH] minor change to test --- .../privacy_preserving_transaction/circuit.rs | 342 +++++++++--------- nssa/src/state.rs | 213 +++++++++++ 2 files changed, 376 insertions(+), 179 deletions(-) diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index f08ceba4..cce9c190 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -177,12 +177,27 @@ mod tests { use nssa_core::{ Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, - SharedSecretKey, + PrivacyPreservingCircuitOutput, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::PrivateAccountKind, program::PdaSeed, }; + fn decrypt_kind( + output: &PrivacyPreservingCircuitOutput, + ssk: &SharedSecretKey, + idx: usize, + ) -> PrivateAccountKind { + let (kind, _) = EncryptionScheme::decrypt( + &output.ciphertexts[idx], + ssk, + &output.new_commitments[idx], + idx as u32, + ) + .unwrap(); + kind + } + use super::*; use crate::{ error::NssaError, @@ -446,189 +461,12 @@ mod tests { ) .unwrap(); - let commitment = output.new_commitments[0].clone(); - let (kind, _account) = - EncryptionScheme::decrypt(&output.ciphertexts[0], &shared_secret, &commitment, 0) - .unwrap(); - assert_eq!( - kind, + decrypt_kind(&output, &shared_secret, 0), PrivateAccountKind::Pda { program_id: program.id(), seed, identifier }, ); } - /// A private PDA family has two members (identifier=0 and identifier=1, same seed/npk). - /// Each is funded in a separate transaction; commitments must be distinct and ciphertexts - /// must carry the correct `PrivateAccountKind::Pda { identifier }`. Alice then spends both. - #[test] - fn two_private_pda_family_members_receive_and_spend() { - let alice_keys = test_private_account_keys_1(); - let alice_npk = alice_keys.npk(); - let recipient_keys = test_private_account_keys_2(); - - let proxy = Program::auth_transfer_proxy(); - let auth_transfer = Program::authenticated_transfer_program(); - let proxy_id = proxy.id(); - let auth_transfer_id = auth_transfer.id(); - let seed = PdaSeed::new([42; 32]); - let amount: u128 = 100; - - let program_with_deps = ProgramWithDependencies::new( - proxy, - [(auth_transfer_id, auth_transfer)].into(), - ); - - let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); - let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1); - - // Public funder account: already owned by auth_transfer so its balance can be debited. - let funder = AccountWithMetadata::new( - Account { program_owner: auth_transfer_id, balance: 500, ..Account::default() }, - true, - AccountId::new([0xAB; 32]), - ); - - let alice_shared_0 = SharedSecretKey::new(&[10; 32], &alice_keys.vpk()); - let alice_shared_1 = SharedSecretKey::new(&[11; 32], &alice_keys.vpk()); - - // Fund alice_pda_0 (identifier = 0) - let (output_recv_0, _) = execute_and_prove( - vec![ - funder.clone(), - AccountWithMetadata::new(Account::default(), false, alice_pda_0_id), - ], - Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { - npk: alice_npk, - ssk: alice_shared_0.clone(), - identifier: 0, - }, - ], - &program_with_deps, - ) - .unwrap(); - - assert_eq!(output_recv_0.new_commitments.len(), 1); - let commitment_pda_0 = output_recv_0.new_commitments[0].clone(); - - let (kind_0, alice_pda_0_account) = EncryptionScheme::decrypt( - &output_recv_0.ciphertexts[0], - &alice_shared_0, - &commitment_pda_0, - 0, - ) - .unwrap(); - assert_eq!( - kind_0, - PrivateAccountKind::Pda { program_id: proxy_id, seed, identifier: 0 }, - ); - assert_eq!(alice_pda_0_account.balance, amount); - - // Fund alice_pda_1 (identifier = 1, same seed) - let (output_recv_1, _) = execute_and_prove( - vec![ - funder.clone(), - AccountWithMetadata::new(Account::default(), false, alice_pda_1_id), - ], - Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { - npk: alice_npk, - ssk: alice_shared_1.clone(), - identifier: 1, - }, - ], - &program_with_deps, - ) - .unwrap(); - - assert_eq!(output_recv_1.new_commitments.len(), 1); - let commitment_pda_1 = output_recv_1.new_commitments[0].clone(); - - let (kind_1, alice_pda_1_account) = EncryptionScheme::decrypt( - &output_recv_1.ciphertexts[0], - &alice_shared_1, - &commitment_pda_1, - 0, - ) - .unwrap(); - assert_eq!( - kind_1, - PrivateAccountKind::Pda { program_id: proxy_id, seed, identifier: 1 }, - ); - assert_eq!(alice_pda_1_account.balance, amount); - - // Different identifiers produce distinct commitments. - assert_ne!(commitment_pda_0, commitment_pda_1); - - // Alice spends alice_pda_0 - let mut cs_0 = CommitmentSet::with_capacity(1); - cs_0.extend(std::slice::from_ref(&commitment_pda_0)); - let proof_pda_0 = cs_0.get_proof_for(&commitment_pda_0); - - let recipient_0_id = AccountId::from((&recipient_keys.npk(), 0u128)); - let (output_spend_0, _) = execute_and_prove( - vec![ - AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id), - AccountWithMetadata::new(Account::default(), false, recipient_0_id), - ], - Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), - vec![ - InputAccountIdentity::PrivatePdaUpdate { - ssk: alice_shared_0.clone(), - nsk: alice_keys.nsk, - membership_proof: proof_pda_0.expect("pda_0 commitment must be in the set"), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[20; 32], &recipient_keys.vpk()), - identifier: 0, - }, - ], - &program_with_deps, - ) - .unwrap(); - - assert_eq!(output_spend_0.new_commitments.len(), 2); - assert_eq!(output_spend_0.new_nullifiers.len(), 2); - - // Alice spends alice_pda_1 - let mut cs_1 = CommitmentSet::with_capacity(1); - cs_1.extend(std::slice::from_ref(&commitment_pda_1)); - let proof_pda_1 = cs_1.get_proof_for(&commitment_pda_1); - - let recipient_1_id = AccountId::from((&recipient_keys.npk(), 1u128)); - let (output_spend_1, _) = execute_and_prove( - vec![ - AccountWithMetadata::new(alice_pda_1_account, true, alice_pda_1_id), - AccountWithMetadata::new(Account::default(), false, recipient_1_id), - ], - Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), - vec![ - InputAccountIdentity::PrivatePdaUpdate { - ssk: alice_shared_1.clone(), - nsk: alice_keys.nsk, - membership_proof: proof_pda_1.expect("pda_1 commitment must be in the set"), - identifier: 1, - }, - InputAccountIdentity::PrivateUnauthorized { - npk: recipient_keys.npk(), - ssk: SharedSecretKey::new(&[21; 32], &recipient_keys.vpk()), - identifier: 1, - }, - ], - &program_with_deps, - ) - .unwrap(); - - assert_eq!(output_spend_1.new_commitments.len(), 2); - assert_eq!(output_spend_1.new_nullifiers.len(), 2); - } - /// Group PDA deposit: creates a new PDA and transfers balance from the /// counterparty. Both accounts owned by `private_pda_spender`. #[test] @@ -732,4 +570,150 @@ mod tests { let (output, _proof) = result.expect("group PDA spend binding should succeed"); assert_eq!(output.new_commitments.len(), 1); } + + /// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Regular` carrying the correct identifier. + #[test] + fn private_authorized_init_encrypts_regular_kind_with_identifier() { + let program = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let account_id = AccountId::from((&keys.npk(), identifier)); + let pre = AccountWithMetadata::new(Account::default(), true, account_id); + + let (output, _) = execute_and_prove( + vec![pre], + Program::serialize_instruction(0_u128).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: ssk.clone(), + nsk: keys.nsk, + identifier, + }], + &program.into(), + ) + .unwrap(); + + assert_eq!(decrypt_kind(&output, &ssk, 0), PrivateAccountKind::Regular(identifier)); + } + + /// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Regular` carrying the correct identifier. + #[test] + fn private_unauthorized_init_encrypts_regular_kind_with_identifier() { + let program = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let sender = AccountWithMetadata::new( + Account { program_owner: program.id(), balance: 1, ..Account::default() }, + true, + AccountId::new([0; 32]), + ); + let recipient_id = AccountId::from((&keys.npk(), identifier)); + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id); + + let (output, _) = execute_and_prove( + vec![sender, recipient], + Program::serialize_instruction(1_u128).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + npk: keys.npk(), + ssk: ssk.clone(), + identifier, + }, + ], + &program.into(), + ) + .unwrap(); + + assert_eq!(decrypt_kind(&output, &ssk, 0), PrivateAccountKind::Regular(identifier)); + } + + /// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Regular` carrying the correct identifier. + #[test] + fn private_authorized_update_encrypts_regular_kind_with_identifier() { + let program = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let account_id = AccountId::from((&keys.npk(), identifier)); + let account = Account { program_owner: program.id(), balance: 1, ..Account::default() }; + let commitment = Commitment::new(&account_id, &account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&commitment)); + + let sender = AccountWithMetadata::new(account, true, account_id); + let recipient = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); + + let (output, _) = execute_and_prove( + vec![sender, recipient], + Program::serialize_instruction(1_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: ssk.clone(), + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), + identifier, + }, + InputAccountIdentity::Public, + ], + &program.into(), + ) + .unwrap(); + + assert_eq!(decrypt_kind(&output, &ssk, 0), PrivateAccountKind::Regular(identifier)); + } + + /// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. + #[test] + fn private_pda_update_encrypts_pda_kind_with_identifier() { + let program = Program::auth_transfer_proxy(); + let auth_transfer = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let identifier: u128 = 99; + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let auth_transfer_id = auth_transfer.id(); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); + let pda_account = + Account { program_owner: auth_transfer_id, balance: 1, ..Account::default() }; + let pda_commitment = Commitment::new(&pda_id, &pda_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&pda_commitment)); + + let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); + let recipient_pre = + AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); + + let program_with_deps = + ProgramWithDependencies::new(program.clone(), [(auth_transfer_id, auth_transfer)].into()); + + let (output, _) = execute_and_prove( + vec![pda_pre, recipient_pre], + Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + ssk: ssk.clone(), + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), + identifier, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Pda { program_id: program.id(), seed, identifier }, + ); + } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 078ce20a..beaf45b5 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -1256,6 +1256,12 @@ pub mod tests { } } + fn test_public_account_keys_2() -> TestPublicKeys { + TestPublicKeys { + signing_key: PrivateKey::try_new([38; 32]).unwrap(), + } + } + pub fn test_private_account_keys_1() -> TestPrivateKeys { TestPrivateKeys { nsk: [13; 32], @@ -4294,4 +4300,211 @@ pub mod tests { "program with spoofed caller_program_id in output should be rejected" ); } + + #[test] + fn two_private_pda_family_members_receive_and_spend() { + let funder_keys = test_public_account_keys_1(); + let alice_keys = test_private_account_keys_1(); + let alice_npk = alice_keys.npk(); + + let proxy = Program::auth_transfer_proxy(); + let auth_transfer = Program::authenticated_transfer_program(); + let proxy_id = proxy.id(); + let auth_transfer_id = auth_transfer.id(); + let seed = PdaSeed::new([42; 32]); + let amount: u128 = 100; + + let program_with_deps = + ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); + + let funder_id = funder_keys.account_id(); + let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); + let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1); + let recipient_id = test_public_account_keys_2().account_id(); + let recipient_signing_key = test_public_account_keys_2().signing_key; + + let mut state = V03State::new_with_genesis_accounts(&[(funder_id, 500)], vec![], 0); + + let alice_pda_0_account = Account { + program_owner: auth_transfer_id, + balance: amount, + nonce: Nonce::private_account_nonce_init(&alice_pda_0_id), + ..Account::default() + }; + let alice_pda_1_account = Account { + program_owner: auth_transfer_id, + balance: amount, + nonce: Nonce::private_account_nonce_init(&alice_pda_1_id), + ..Account::default() + }; + + let alice_shared_0 = SharedSecretKey::new(&[10; 32], &alice_keys.vpk()); + let alice_shared_1 = SharedSecretKey::new(&[11; 32], &alice_keys.vpk()); + + // Fund alice_pda_0 + { + let funder_account = state.get_account_by_id(funder_id); + let funder_nonce = funder_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(funder_account, true, funder_id), + AccountWithMetadata::new(Account::default(), false, alice_pda_0_id), + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + npk: alice_npk, + ssk: alice_shared_0.clone(), + identifier: 0, + }, + ], + &program_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![funder_id], + vec![funder_nonce], + vec![(alice_npk, alice_keys.vpk(), EphemeralPublicKey::from_scalar([10; 32]))], + output, + ) + .unwrap(); + let witness_set = + WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 1, + 0, + ) + .unwrap(); + } + + // Fund alice_pda_1 + { + let funder_account = state.get_account_by_id(funder_id); + let funder_nonce = funder_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(funder_account, true, funder_id), + AccountWithMetadata::new(Account::default(), false, alice_pda_1_id), + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + npk: alice_npk, + ssk: alice_shared_1.clone(), + identifier: 1, + }, + ], + &program_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![funder_id], + vec![funder_nonce], + vec![(alice_npk, alice_keys.vpk(), EphemeralPublicKey::from_scalar([11; 32]))], + output, + ) + .unwrap(); + let witness_set = + WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 2, + 0, + ) + .unwrap(); + } + + let commitment_pda_0 = Commitment::new(&alice_pda_0_id, &alice_pda_0_account); + let commitment_pda_1 = Commitment::new(&alice_pda_1_id, &alice_pda_1_account); + + assert!(state.get_proof_for_commitment(&commitment_pda_0).is_some()); + assert!(state.get_proof_for_commitment(&commitment_pda_1).is_some()); + + // Alice spends alice_pda_0 into the public recipient. + { + let recipient_account = state.get_account_by_id(recipient_id); + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id), + AccountWithMetadata::new(recipient_account, true, recipient_id), + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + ssk: alice_shared_0, + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_0) + .expect("pda_0 must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![recipient_id], + vec![Nonce(0)], + vec![(alice_npk, alice_keys.vpk(), EphemeralPublicKey::from_scalar([10; 32]))], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 3, + 0, + ) + .unwrap(); + } + + // Alice spends alice_pda_1 into the same public recipient. + { + let recipient_account = state.get_account_by_id(recipient_id); + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(alice_pda_1_account, true, alice_pda_1_id), + AccountWithMetadata::new(recipient_account, false, recipient_id), + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + ssk: alice_shared_1, + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_1) + .expect("pda_1 must be in state"), + identifier: 1, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![recipient_id], + vec![], + vec![(alice_npk, alice_keys.vpk(), EphemeralPublicKey::from_scalar([11; 32]))], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 4, + 0, + ) + .unwrap(); + } + + assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount); + } }