diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 148a9403..dbb3ecac 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index 46326067..efe57235 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index ad40805f..1810c0b5 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index e2a6f120..81ad7b44 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index d0460713..3286a68a 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index b0f81f79..db2a30d4 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index dcbee51a..388b79f6 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index e0358fa4..26e7dc81 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index 9bd40a30..9dabdc7a 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 0353d78f..656824c9 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index cd74cf3f..c08eaf31 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index 1f966bef..4fd119bf 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 8a48effd..d38c7ef5 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index e08df712..970f8c4d 100644 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 37abf0f7..a21647a6 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index ebd53621..6fc41e7e 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index 29c660cd..85da071f 100644 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin index a560d477..1e927773 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index c9d0facd..8885831a 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin index 9b31fd7e..9dccca85 100644 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin index c4a2c039..8802a9b6 100644 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 42d2171d..b2e73618 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index d2b99291..087fd065 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index f57ac2f1..526edd06 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 6b79e074..3bb367a8 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index eb89f4a9..b6f3814d 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin index 092a2191..40575bce 100644 Binary files a/artifacts/test_program_methods/pda_claimer.bin and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 559adea4..8c28ebdb 100644 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index d7e81a9f..50509a6a 100644 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 880e03b1..cf577e5a 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 3a4e811f..51a8f4ea 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin index eeb80385..964280b5 100644 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin index b71d87ab..2b2ef93b 100644 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index 8d749f3c..401f7189 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index 109829d2..1dbdf2ea 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 7d2a6d29..b9c73e4e 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -27,7 +27,7 @@ use nssa::{ public_transaction as putx, }; use nssa_core::{ - MembershipProof, NullifierPublicKey, + MembershipProof, NullifierPublicKey, PrivacyPreservingCircuitInputAccount, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -248,10 +248,17 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], - vec![(sender_npk, sender_ss), (recipient_npk, recipient_ss)], - vec![sender_nsk], - vec![Some(proof)], + vec![ + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: sender_ss, + nsk: sender_nsk, + membership_proof: proof, + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_npk, + ssk: recipient_ss, + }, + ], &program.into(), ) .unwrap(); diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 215c7db8..a6f6c9c3 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -12,23 +12,83 @@ use crate::{ pub struct PrivacyPreservingCircuitInput { /// Outputs of the program execution. pub program_outputs: Vec, - /// Visibility mask for accounts. - /// - /// - `0` - public account - /// - `1` - private account with authentication - /// - `2` - private account without authentication - /// - `3` - private PDA account - pub visibility_mask: Vec, - /// Public keys of private accounts. - pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, - /// Nullifier secret keys for authorized private accounts. - pub private_account_nsks: Vec, - /// Membership proofs for private accounts. Can be [`None`] for uninitialized accounts. - pub private_account_membership_proofs: Vec>, + /// One entry per `pre_state`, in the same order as the program's `pre_states`. + /// Length must equal the number of `pre_states` derived from `program_outputs`. + /// The guest's `private_pda_npk_by_position` and `private_pda_bound_positions` + /// rely on this position alignment. + pub accounts: Vec, /// Program ID. pub program_id: ProgramId, } +/// Per-account input to the privacy-preserving circuit. Each variant carries exactly the fields +/// the guest needs for that account's code path. +#[derive(Serialize, Deserialize, Clone)] +pub enum PrivacyPreservingCircuitInputAccount { + /// Public account. The guest reads pre/post state from `program_outputs` and emits no + /// commitment, ciphertext, or nullifier. + Public, + /// Init of an authorized standalone private account (mask 1, no membership proof). The + /// `pre_state` must be `Account::default()`. `npk` is derived from `nsk` and matched + /// against `pre_state.account_id` via `AccountId::from(npk)`. + PrivateAuthorizedInit { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + }, + /// Update of an authorized standalone private account (mask 1, with membership proof). + PrivateAuthorizedUpdate { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + membership_proof: MembershipProof, + }, + /// Unauthorized init of a standalone private account (mask 2). Used for recipients who + /// don't yet exist on chain. No `nsk`, no membership proof. + PrivateUnauthorized { + npk: NullifierPublicKey, + ssk: SharedSecretKey, + }, + /// Init of a private PDA (mask 3, unauthorized). The npk-to-account_id binding is proven + /// upstream via `Claim::Pda(seed)` or a caller's `pda_seeds` match. + PrivatePdaInit { + npk: NullifierPublicKey, + ssk: SharedSecretKey, + }, + /// Update of an existing private PDA (mask 3, authorized, with membership proof). `npk` is + /// derived from `nsk`. Authorization is established upstream by a caller `pda_seeds` match + /// or a previously-seen authorization in a chained call. + PrivatePdaUpdate { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + membership_proof: MembershipProof, + }, +} + +impl PrivacyPreservingCircuitInputAccount { + #[must_use] + pub const fn is_public(&self) -> bool { + matches!(self, Self::Public) + } + + #[must_use] + pub const fn is_private_pda(&self) -> bool { + matches!(self, Self::PrivatePdaInit { .. } | Self::PrivatePdaUpdate { .. }) + } + + /// For private PDA variants, return the nullifier public key. `Init` carries it directly; + /// `Update` derives it from `nsk`. For non-PDA variants returns `None`. + #[must_use] + pub fn npk_if_private_pda(&self) -> Option { + match self { + Self::PrivatePdaInit { npk, .. } => Some(*npk), + Self::PrivatePdaUpdate { nsk, .. } => Some(NullifierPublicKey::from(nsk)), + Self::Public + | Self::PrivateAuthorizedInit { .. } + | Self::PrivateAuthorizedUpdate { .. } + | Self::PrivateUnauthorized { .. } => None, + } + } +} + #[derive(Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct PrivacyPreservingCircuitOutput { diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index a4fcdee1..405cab6d 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -3,7 +3,10 @@ reason = "We prefer to group methods by functionality rather than by type for encoding" )] -pub use circuit_io::{PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput}; +pub use circuit_io::{ + PrivacyPreservingCircuitInput, PrivacyPreservingCircuitInputAccount, + PrivacyPreservingCircuitOutput, +}; pub use commitment::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, compute_digest_for_path, diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 528bb372..57c9185f 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -2,8 +2,8 @@ use std::collections::{HashMap, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, - PrivacyPreservingCircuitOutput, SharedSecretKey, + PrivacyPreservingCircuitInput, PrivacyPreservingCircuitInputAccount, + PrivacyPreservingCircuitOutput, account::AccountWithMetadata, program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, }; @@ -63,14 +63,10 @@ impl From for ProgramWithDependencies { /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution /// circuit. -/// TODO: too many parameters. pub fn execute_and_prove( pre_states: Vec, instruction_data: InstructionData, - visibility_mask: Vec, - private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, - private_account_nsks: Vec, - private_account_membership_proofs: Vec>, + accounts: Vec, program_with_dependencies: &ProgramWithDependencies, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { let ProgramWithDependencies { @@ -128,10 +124,7 @@ pub fn execute_and_prove( let circuit_input = PrivacyPreservingCircuitInput { program_outputs, - visibility_mask, - private_account_keys, - private_account_nsks, - private_account_membership_proofs, + accounts, program_id: program_with_dependencies.program.id(), }; @@ -243,10 +236,13 @@ mod tests { let (output, proof) = execute_and_prove( vec![sender, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![0, 2], - vec![(recipient_keys.npk(), shared_secret)], - vec![], - vec![None], + vec![ + PrivacyPreservingCircuitInputAccount::Public, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret, + }, + ], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -339,13 +335,19 @@ mod tests { let (output, proof) = execute_and_prove( vec![sender_pre, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], vec![ - (sender_keys.npk(), shared_secret_1), - (recipient_keys.npk(), shared_secret_2), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: shared_secret_1, + nsk: sender_keys.nsk, + membership_proof: commitment_set + .get_proof_for(&commitment_sender) + .expect("sender's commitment must be in the set"), + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret_2, + }, ], - vec![sender_keys.nsk], - vec![commitment_set.get_proof_for(&commitment_sender), None], &program.into(), ) .unwrap(); @@ -408,10 +410,10 @@ mod tests { let result = execute_and_prove( vec![pre], instruction, - vec![2], - vec![(account_keys.npk(), shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + }], &program_with_deps, ); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index f86f429f..97b3eef0 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -362,8 +362,8 @@ pub mod tests { use std::collections::HashMap; use nssa_core::{ - BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - Timestamp, + BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, + PrivacyPreservingCircuitInputAccount, SharedSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, program::{ @@ -1292,10 +1292,13 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![0, 2], - vec![(recipient_keys.npk(), shared_secret)], - vec![], - vec![None], + vec![ + PrivacyPreservingCircuitInputAccount::Public, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret, + }, + ], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -1337,13 +1340,19 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], vec![ - (sender_keys.npk(), shared_secret_1), - (recipient_keys.npk(), shared_secret_2), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: shared_secret_1, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret_2, + }, ], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment), None], &program.into(), ) .unwrap(); @@ -1388,10 +1397,16 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 0], - vec![(sender_keys.npk(), shared_secret)], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment)], + vec![ + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + }, + PrivacyPreservingCircuitInputAccount::Public, + ], &program.into(), ) .unwrap(); @@ -1602,10 +1617,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(10_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); @@ -1628,10 +1640,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(10_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); @@ -1654,10 +1663,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); @@ -1680,10 +1686,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(vec![0]).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); @@ -1714,10 +1717,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(large_data).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); @@ -1740,10 +1740,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); @@ -1775,10 +1772,10 @@ pub mod tests { let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(()).unwrap(), - vec![0, 0], - vec![], - vec![], - vec![], + vec![ + PrivacyPreservingCircuitInputAccount::Public, + PrivacyPreservingCircuitInputAccount::Public, + ], &program.into(), ); @@ -1801,10 +1798,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); @@ -1836,10 +1830,10 @@ pub mod tests { let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![0, 0], - vec![], - vec![], - vec![], + vec![ + PrivacyPreservingCircuitInputAccount::Public, + PrivacyPreservingCircuitInputAccount::Public, + ], &program.into(), ); @@ -1868,175 +1862,21 @@ pub mod tests { AccountId::new([1; 32]), ); - // Setting only one visibility mask for a circuit execution with two pre_state accounts. - let visibility_mask = [0]; + // Single account entry for a circuit execution with two pre_state accounts. let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - #[test] - fn circuit_fails_if_insufficient_nonces_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - &sender_keys.npk(), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_keys_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - &sender_keys.npk(), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); - - // Setting only one key for an execution with two private accounts. - let private_account_keys = [( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - )]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_commitment_proofs_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - &sender_keys.npk(), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); - - // Setting no second commitment proof. - let private_account_membership_proofs = [Some((0, vec![]))]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - private_account_membership_proofs.to_vec(), - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_auth_keys_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - &sender_keys.npk(), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); - - // Setting no auth key for an execution with one non default private accounts. - let private_account_nsks = []; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - private_account_nsks.to_vec(), - vec![], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } + // The four `circuit_fails_if_insufficient_*_are_provided` tests have been removed in this + // refactor: each variant of `PrivacyPreservingCircuitInputAccount` carries exactly the fields + // the corresponding circuit code path needs, so the parallel-vec length mismatches those + // tests exercised are now compile-time impossibilities. #[test] fn circuit_fails_if_invalid_auth_keys_are_provided() { @@ -2055,31 +1895,24 @@ pub mod tests { let private_account_2 = AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); - let private_account_keys = [ - // First private account is the sender - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - // Second private account is the recipient - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ]; - - // Setting the recipient key to authorize the sender. - // This should be set to the sender private account in - // a normal circumstance. The recipient can't authorize this. - let private_account_nsks = [recipient_keys.nsk]; - let private_account_membership_proofs = [Some((0, vec![]))]; + // Setting the recipient nsk to authorize the sender. + // This should be set to the sender private account in a normal circumstance. + // `PrivateAuthorizedUpdate` derives npk from nsk and asserts equality with + // `pre_state.account_id`, so a mismatched nsk fails that check. let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), + vec![ + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: recipient_keys.nsk, + membership_proof: (0, vec![]), + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + }, + ], &program.into(), ); @@ -2113,19 +1946,17 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2159,19 +1990,17 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2205,19 +2034,17 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2251,19 +2078,17 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2295,19 +2120,17 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2336,14 +2159,16 @@ pub mod tests { let private_pda_account = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); - let visibility_mask = [0, 3]; let result = execute_and_prove( vec![public_account_1, private_pda_account], Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![(npk, shared_secret)], - vec![], - vec![None], + vec![ + PrivacyPreservingCircuitInputAccount::Public, + PrivacyPreservingCircuitInputAccount::PrivatePdaInit { + npk, + ssk: shared_secret, + }, + ], &program.into(), ); @@ -2369,10 +2194,10 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![3], - vec![(npk, shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivatePdaInit { + npk, + ssk: shared_secret, + }], &program.into(), ); @@ -2407,10 +2232,10 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![3], - vec![(npk_b, shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivatePdaInit { + npk: npk_b, + ssk: shared_secret, + }], &program.into(), ); @@ -2441,10 +2266,10 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction((seed, seed, callee_id)).unwrap(), - vec![3], - vec![(npk, shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivatePdaInit { + npk, + ssk: shared_secret, + }], &program_with_deps, ); @@ -2478,10 +2303,10 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), - vec![3], - vec![(npk, shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivatePdaInit { + npk, + ssk: shared_secret, + }], &program_with_deps, ); @@ -2514,10 +2339,16 @@ pub mod tests { let result = execute_and_prove( vec![pre_a, pre_b], Program::serialize_instruction(seed).unwrap(), - vec![3, 3], - vec![(keys_a.npk(), shared_a), (keys_b.npk(), shared_b)], - vec![], - vec![None, None], + vec![ + PrivacyPreservingCircuitInputAccount::PrivatePdaInit { + npk: keys_a.npk(), + ssk: shared_a, + }, + PrivacyPreservingCircuitInputAccount::PrivatePdaInit { + npk: keys_b.npk(), + ssk: shared_b, + }, + ], &program.into(), ); @@ -2558,144 +2389,20 @@ pub mod tests { let result = execute_and_prove( vec![owned_pre_state], Program::serialize_instruction(()).unwrap(), - vec![3], - vec![(npk, shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivatePdaInit { + npk, + ssk: shared_secret, + }], &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - #[test] - fn circuit_should_fail_with_too_many_nonces() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - &sender_keys.npk(), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_with_too_many_private_account_keys() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - &sender_keys.npk(), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); - - // Setting three private account keys for a circuit execution with only two private - // accounts. - let private_account_keys = [ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ( - sender_keys.npk(), - SharedSecretKey::new(&[57; 32], &sender_keys.vpk()), - ), - ]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_with_too_many_private_account_auth_keys() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - &sender_keys.npk(), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); - - // Setting two private account keys for a circuit execution with only one non default - // private account (visibility mask equal to 1 means that auth keys are expected). - let visibility_mask = [1, 2]; - let private_account_nsks = [sender_keys.nsk, recipient_keys.nsk]; - let private_account_membership_proofs = [Some((0, vec![])), Some((1, vec![]))]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![ - ( - sender_keys.npk(), - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } + // The three `circuit_should_fail_with_too_many_*` tests have been removed in this refactor: + // each variant of `PrivacyPreservingCircuitInputAccount` carries exactly the fields the + // corresponding circuit code path needs, so the parallel-vec over-supply mismatches those + // tests exercised are now compile-time impossibilities. #[test] fn private_accounts_can_only_be_initialized_once() { @@ -2767,20 +2474,22 @@ pub mod tests { &sender_keys.npk(), ); - let visibility_mask = [1, 1]; - let private_account_nsks = [sender_keys.nsk, sender_keys.nsk]; - let private_account_membership_proofs = [Some((1, vec![])), Some((1, vec![]))]; let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.vpk()); let result = execute_and_prove( vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), - visibility_mask.to_vec(), vec![ - (sender_keys.npk(), shared_secret), - (sender_keys.npk(), shared_secret), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + }, + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + }, ], - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), &program.into(), ); @@ -3071,10 +2780,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(0_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![PrivacyPreservingCircuitInputAccount::Public], &program.into(), ); @@ -3111,10 +2817,16 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(37_u128).unwrap(), - vec![1, 0], - vec![(sender_keys.npk(), shared_secret)], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment)], + vec![ + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + }, + PrivacyPreservingCircuitInputAccount::Public, + ], &program.into(), ) .unwrap(); @@ -3230,12 +2942,21 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![to_account, from_account], Program::serialize_instruction(instruction).unwrap(), - vec![1, 1], - vec![(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)], - vec![from_keys.nsk, to_keys.nsk], vec![ - state.get_proof_for_commitment(&from_commitment), - state.get_proof_for_commitment(&to_commitment), + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: to_ss, + nsk: from_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&from_commitment) + .expect("from's commitment must be in state"), + }, + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: from_ss, + nsk: to_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&to_commitment) + .expect("to's commitment must be in state"), + }, ], &program_with_deps, ) @@ -3499,10 +3220,10 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![authorized_account], Program::serialize_instruction(balance).unwrap(), - vec![1], - vec![(private_keys.npk(), shared_secret)], - vec![private_keys.nsk], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivateAuthorizedInit { + ssk: shared_secret, + nsk: private_keys.nsk, + }], &program.into(), ) .unwrap(); @@ -3546,10 +3267,10 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![unauthorized_account], Program::serialize_instruction(0_u128).unwrap(), - vec![2], - vec![(private_keys.npk(), shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: private_keys.npk(), + ssk: shared_secret, + }], &program.into(), ) .unwrap(); @@ -3597,10 +3318,10 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![authorized_account.clone()], Program::serialize_instruction(balance).unwrap(), - vec![1], - vec![(private_keys.npk(), shared_secret)], - vec![private_keys.nsk], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivateAuthorizedInit { + ssk: shared_secret, + nsk: private_keys.nsk, + }], &claimer_program.into(), ) .unwrap(); @@ -3642,10 +3363,10 @@ pub mod tests { let res = execute_and_prove( vec![account_metadata], Program::serialize_instruction(()).unwrap(), - vec![1], - vec![(private_keys.npk(), shared_secret2)], - vec![private_keys.nsk], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivateAuthorizedInit { + ssk: shared_secret2, + nsk: private_keys.nsk, + }], &noop_program.into(), ); @@ -3718,13 +3439,11 @@ pub mod tests { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![1], - vec![( - sender_keys.npk(), - SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), - )], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], + vec![PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }], &program.into(), ); @@ -3745,13 +3464,11 @@ pub mod tests { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![1], - vec![( - sender_keys.npk(), - SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), - )], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], + vec![PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }], &program.into(), ); @@ -3803,10 +3520,16 @@ pub mod tests { let result = execute_and_prove( vec![sender_account, recipient_account], Program::serialize_instruction(instruction).unwrap(), - vec![0, 1], - vec![(recipient_keys.npk(), recipient)], - vec![recipient_keys.nsk], - vec![state.get_proof_for_commitment(&recipient_commitment)], + vec![ + PrivacyPreservingCircuitInputAccount::Public, + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: recipient, + nsk: recipient_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&recipient_commitment) + .expect("recipient's commitment must be in state"), + }, + ], &program_with_deps, ); @@ -3953,10 +3676,10 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), - vec![2], - vec![(account_keys.npk(), shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + }], &validity_window_program.into(), ) .unwrap(); @@ -4022,10 +3745,10 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), - vec![2], - vec![(account_keys.npk(), shared_secret)], - vec![], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + }], &validity_window_program.into(), ) .unwrap(); diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 8018cd80..b9327f71 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -6,7 +6,7 @@ use std::{ use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, - PrivacyPreservingCircuitOutput, SharedSecretKey, + PrivacyPreservingCircuitInputAccount, PrivacyPreservingCircuitOutput, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce}, compute_digest_for_path, program::{ @@ -43,39 +43,28 @@ struct ExecutionState { /// `AccountId` entry or as an equality check against the existing one, making the rule: one /// `(program, seed)` → one account per tx. pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>, - /// Map from a mask-3 `pre_state`'s position in `visibility_mask` to the npk supplied for - /// that position in `private_account_keys`. Built once in `derive_from_outputs` by walking - /// `visibility_mask` in lock-step with `private_account_keys`, used later by the claim and - /// caller-seeds authorization paths. + /// Map from a private-PDA `pre_state`'s position in `accounts` to the npk that variant + /// supplies for that position. Populated once in `derive_from_outputs` by walking + /// `accounts` and consulting `npk_if_private_pda`. Used later by the claim and + /// caller-seeds authorization paths to verify + /// `AccountId::for_private_pda(program_id, seed, npk) == pre_state.account_id`. private_pda_npk_by_position: HashMap, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs( - visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], + accounts: &[PrivacyPreservingCircuitInputAccount], program_id: ProgramId, program_outputs: Vec, ) -> Self { - // Build position → npk map for mask-3 pre_states. `private_account_keys` is consumed in - // pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The - // downstream `compute_circuit_output` also consumes the same iterator and its trailing - // assertions catch an over-supply of keys; under-supply surfaces here. + // Build position → npk map for private-PDA pre_states, indexed by position in `accounts`. + // The `accounts` vec is documented as 1:1 with the program's pre_state order, so position + // here matches `pre_state_position` used downstream in `validate_and_sync_states`. let mut private_pda_npk_by_position: HashMap = HashMap::new(); - { - let mut keys_iter = private_account_keys.iter(); - for (pos, &mask) in visibility_mask.iter().enumerate() { - if matches!(mask, 1..=3) { - let (npk, _) = keys_iter.next().unwrap_or_else(|| { - panic!( - "private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})" - ) - }); - if mask == 3 { - private_pda_npk_by_position.insert(pos, *npk); - } - } + for (pos, account) in accounts.iter().enumerate() { + if let Some(npk) = account.npk_if_private_pda() { + private_pda_npk_by_position.insert(pos, npk); } } @@ -192,7 +181,7 @@ impl ExecutionState { } execution_state.validate_and_sync_states( - visibility_mask, + accounts, chained_call.program_id, caller_program_id, &chained_call.pda_seeds, @@ -209,12 +198,12 @@ impl ExecutionState { "Inner call without a chained call found", ); - // Every mask-3 pre_state must have had its npk bound to its account_id, either via a - // `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` matching - // the private derivation. An unbound mask-3 pre_state has no cryptographic link between - // the supplied npk and the account_id, and must be rejected. - for (pos, &mask) in visibility_mask.iter().enumerate() { - if mask == 3 { + // Every private-PDA pre_state must have had its npk bound to its account_id, either via + // a `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` + // matching the private derivation. An unbound private-PDA pre_state has no + // cryptographic link between the supplied npk and the account_id, and must be rejected. + for (pos, account) in accounts.iter().enumerate() { + if account.is_private_pda() { assert!( execution_state.private_pda_bound_positions.contains(&pos), "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" @@ -249,7 +238,7 @@ impl ExecutionState { /// Validate program pre and post states and populate the execution state. fn validate_and_sync_states( &mut self, - visibility_mask: &[u8], + accounts: &[PrivacyPreservingCircuitInputAccount], program_id: ProgramId, caller_program_id: Option, caller_pda_seeds: &[PdaSeed], @@ -327,9 +316,9 @@ impl ExecutionState { .position(|acc| acc.account_id == pre_account_id) .expect("Pre state must exist at this point"); - let mask = visibility_mask[pre_state_position]; - match mask { - 0 => match claim { + let account = &accounts[pre_state_position]; + if account.is_public() { + match claim { Claim::Authorized => { // Note: no need to check authorized pdas because we have already // checked consistency of authorization above. @@ -351,40 +340,38 @@ impl ExecutionState { pre_account_id, ); } - }, - 3 => { - match claim { - Claim::Authorized => { - assert!( - pre_is_authorized, - "Cannot claim unauthorized private PDA {pre_account_id}" - ); - } - Claim::Pda(seed) => { - let npk = self + } + } else if account.is_private_pda() { + match claim { + Claim::Authorized => { + assert!( + pre_is_authorized, + "Cannot claim unauthorized private PDA {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let npk = self .private_pda_npk_by_position .get(&pre_state_position) .expect("private PDA pre_state must have an npk in the position map"); - let pda = AccountId::for_private_pda(&program_id, &seed, npk); - assert_eq!( - pre_account_id, pda, - "Invalid private PDA claim for account {pre_account_id}" - ); - self.private_pda_bound_positions.insert(pre_state_position); - assert_family_binding( - &mut self.pda_family_binding, - program_id, - seed, - pre_account_id, - ); - } + let pda = AccountId::for_private_pda(&program_id, &seed, npk); + assert_eq!( + pre_account_id, pda, + "Invalid private PDA claim for account {pre_account_id}" + ); + self.private_pda_bound_positions.insert(pre_state_position); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); } } - _ => { - // Mask 1/2: standard private accounts don't enforce the claim semantics. - // Unauthorized private claiming is intentionally allowed since operating - // these accounts requires the npk/nsk keypair anyway. - } + } else { + // Standalone private accounts (mask 1/2): don't enforce the claim semantics. + // Unauthorized private claiming is intentionally allowed since operating + // these accounts requires the npk/nsk keypair anyway. } post.account_mut().program_owner = program_id; @@ -486,10 +473,7 @@ fn resolve_authorization_and_record_bindings( fn compute_circuit_output( execution_state: ExecutionState, - visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], - private_account_nsks: &[NullifierSecretKey], - private_account_membership_proofs: &[Option], + accounts: &[PrivacyPreservingCircuitInputAccount], ) -> PrivacyPreservingCircuitOutput { let mut output = PrivacyPreservingCircuitOutput { public_pre_states: Vec::new(), @@ -503,280 +487,241 @@ fn compute_circuit_output( let states_iter = execution_state.into_states_iter(); assert_eq!( - visibility_mask.len(), + accounts.len(), states_iter.len(), - "Invalid visibility mask length" + "Invalid accounts length" ); - let mut private_keys_iter = private_account_keys.iter(); - let mut private_nsks_iter = private_account_nsks.iter(); - let mut private_membership_proofs_iter = private_account_membership_proofs.iter(); - let mut output_index = 0; - for (account_visibility_mask, (pre_state, post_state)) in - visibility_mask.iter().copied().zip(states_iter) - { - match account_visibility_mask { - 0 => { - // Public account + for (account, (pre_state, post_state)) in accounts.iter().zip(states_iter) { + match account { + PrivacyPreservingCircuitInputAccount::Public => { output.public_pre_states.push(pre_state); output.public_post_states.push(post_state); } - 1 | 2 => { - let Some((npk, shared_secret)) = private_keys_iter.next() else { - panic!("Missing private account key"); - }; + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedInit { ssk, nsk } => { + let npk = NullifierPublicKey::from(nsk); + assert_eq!( + AccountId::from(&npk), + pre_state.account_id, + "AccountId mismatch" + ); + assert!( + pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); + assert_eq!( + pre_state.account, + Account::default(), + "Found new private account with non default values" + ); + + let new_nullifier = ( + Nullifier::for_account_initialization(&npk), + DUMMY_COMMITMENT_HASH, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &npk, + ssk, + new_nullifier, + new_nonce, + ); + } + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk, + nsk, + membership_proof, + } => { + let npk = NullifierPublicKey::from(nsk); + + assert_eq!( + AccountId::from(&npk), + pre_state.account_id, + "AccountId mismatch" + ); + assert!( + pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); + + let new_nullifier = compute_update_nullifier_and_set_digest( + membership_proof, + &pre_state.account, + &npk, + nsk, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &npk, + ssk, + new_nullifier, + new_nonce, + ); + } + PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { npk, ssk } => { assert_eq!( AccountId::from(npk), pre_state.account_id, "AccountId mismatch" ); - - let (new_nullifier, new_nonce) = if account_visibility_mask == 1 { - // Private account with authentication - - let Some(nsk) = private_nsks_iter.next() else { - panic!("Missing private account nullifier secret key"); - }; - - // Verify the nullifier public key - assert_eq!( - npk, - &NullifierPublicKey::from(nsk), - "Nullifier public key mismatch" - ); - - // Check pre_state authorization - assert!( - pre_state.is_authorized, - "Pre-state not authorized for authenticated private account" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - let new_nullifier = compute_nullifier_and_set_digest( - membership_proof_opt.as_ref(), - &pre_state.account, - npk, - nsk, - ); - - let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - - (new_nullifier, new_nonce) - } else { - // Private account without authentication - - assert_eq!( - pre_state.account, - Account::default(), - "Found new private account with non default values", - ); - - assert!( - !pre_state.is_authorized, - "Found new private account marked as authorized." - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - assert!( - membership_proof_opt.is_none(), - "Membership proof must be None for unauthorized accounts" - ); - - let nullifier = Nullifier::for_account_initialization(npk); - - let new_nonce = Nonce::private_account_nonce_init(npk); - - ((nullifier, DUMMY_COMMITMENT_HASH), new_nonce) - }; - output.new_nullifiers.push(new_nullifier); - - // Update post-state with new nonce - let mut post_with_updated_nonce = post_state; - post_with_updated_nonce.nonce = new_nonce; - - // Compute commitment - let commitment_post = Commitment::new(npk, &post_with_updated_nonce); - - // Encrypt and push post state - let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, - shared_secret, - &commitment_post, - output_index, + assert_eq!( + pre_state.account, + Account::default(), + "Found new private account with non default values", + ); + assert!( + !pre_state.is_authorized, + "Found new private account marked as authorized." ); - output.new_commitments.push(commitment_post); - output.ciphertexts.push(encrypted_account); - output_index = output_index - .checked_add(1) - .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); + let new_nullifier = ( + Nullifier::for_account_initialization(npk), + DUMMY_COMMITMENT_HASH, + ); + let new_nonce = Nonce::private_account_nonce_init(npk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + npk, + ssk, + new_nullifier, + new_nonce, + ); } - 3 => { - // Private PDA account. The supplied npk has already been bound to - // `pre_state.account_id` upstream in `validate_and_sync_states`, either via a - // `Claim::Pda(seed)` match or via a caller `pda_seeds` match, both of which - // assert `AccountId::for_private_pda(owner, seed, npk) == account_id`. The - // post-loop assertion in `derive_from_outputs` (see the - // `private_pda_bound_positions` check) guarantees that every mask-3 - // position has been through at least one such binding, so this - // branch can safely use the wallet npk without re-verifying. - let Some((npk, shared_secret)) = private_keys_iter.next() else { - panic!("Missing private account key"); - }; - - let (new_nullifier, new_nonce) = if pre_state.is_authorized { - // Existing private PDA with authentication (like mask 1) - let Some(nsk) = private_nsks_iter.next() else { - panic!("Missing private account nullifier secret key"); - }; - assert_eq!( - npk, - &NullifierPublicKey::from(nsk), - "Nullifier public key mismatch" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - let new_nullifier = compute_nullifier_and_set_digest( - membership_proof_opt.as_ref(), - &pre_state.account, - npk, - nsk, - ); - let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - (new_nullifier, new_nonce) - } else { - // New private PDA (like mask 2). The default + unauthorized requirement - // here rules out use cases like a fully-private multisig, which would need - // a non-default, non-authorized private PDA input account. - // TODO(private-pdas-pr-2/3): relax this once the wallet can supply a - // `(seed, owner)` side input so the npk-to-account_id binding can be - // re-verified for an existing private PDA without a `Claim::Pda` or caller - // `pda_seeds` match. - assert_eq!( - pre_state.account, - Account::default(), - "New private PDA must be default" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - assert!( - membership_proof_opt.is_none(), - "Membership proof must be None for new accounts" - ); - - let nullifier = Nullifier::for_account_initialization(npk); - let new_nonce = Nonce::private_account_nonce_init(npk); - ((nullifier, DUMMY_COMMITMENT_HASH), new_nonce) - }; - output.new_nullifiers.push(new_nullifier); - - let mut post_with_updated_nonce = post_state; - post_with_updated_nonce.nonce = new_nonce; - - let commitment_post = Commitment::new(npk, &post_with_updated_nonce); - - let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, - shared_secret, - &commitment_post, - output_index, + PrivacyPreservingCircuitInputAccount::PrivatePdaInit { npk, ssk } => { + // The npk-to-account_id binding is established upstream in + // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` + // match. Here we only enforce the init pre-conditions. + assert!( + !pre_state.is_authorized, + "PrivatePdaInit requires unauthorized pre_state" + ); + assert_eq!( + pre_state.account, + Account::default(), + "New private PDA must be default" ); - output.new_commitments.push(commitment_post); - output.ciphertexts.push(encrypted_account); - output_index = output_index - .checked_add(1) - .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); + let new_nullifier = ( + Nullifier::for_account_initialization(npk), + DUMMY_COMMITMENT_HASH, + ); + let new_nonce = Nonce::private_account_nonce_init(npk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + npk, + ssk, + new_nullifier, + new_nonce, + ); + } + PrivacyPreservingCircuitInputAccount::PrivatePdaUpdate { + ssk, + nsk, + membership_proof, + } => { + let npk = NullifierPublicKey::from(nsk); + + // The npk binding is established upstream. Authorization must already be set; + // an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an + // unbound PDA, which the upstream binding check would have rejected anyway, + // but we assert here to fail fast and document the precondition. + assert!( + pre_state.is_authorized, + "PrivatePdaUpdate requires authorized pre_state" + ); + + let new_nullifier = compute_update_nullifier_and_set_digest( + membership_proof, + &pre_state.account, + &npk, + nsk, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &npk, + ssk, + new_nullifier, + new_nonce, + ); } - _ => panic!("Invalid visibility mask value"), } } - assert!( - private_keys_iter.next().is_none(), - "Too many private account keys" - ); - - assert!( - private_nsks_iter.next().is_none(), - "Too many private account nullifier secret keys" - ); - - assert!( - private_membership_proofs_iter.next().is_none(), - "Too many private account membership proofs" - ); - output } -fn compute_nullifier_and_set_digest( - membership_proof_opt: Option<&MembershipProof>, +fn emit_private_output( + output: &mut PrivacyPreservingCircuitOutput, + output_index: &mut u32, + post_state: Account, + npk: &NullifierPublicKey, + shared_secret: &SharedSecretKey, + new_nullifier: (Nullifier, CommitmentSetDigest), + new_nonce: Nonce, +) { + output.new_nullifiers.push(new_nullifier); + + let mut post_with_updated_nonce = post_state; + post_with_updated_nonce.nonce = new_nonce; + + let commitment_post = Commitment::new(npk, &post_with_updated_nonce); + let encrypted_account = EncryptionScheme::encrypt( + &post_with_updated_nonce, + shared_secret, + &commitment_post, + *output_index, + ); + + output.new_commitments.push(commitment_post); + output.ciphertexts.push(encrypted_account); + *output_index = output_index + .checked_add(1) + .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); +} + +fn compute_update_nullifier_and_set_digest( + membership_proof: &MembershipProof, pre_account: &Account, npk: &NullifierPublicKey, nsk: &NullifierSecretKey, ) -> (Nullifier, CommitmentSetDigest) { - membership_proof_opt.as_ref().map_or_else( - || { - assert_eq!( - *pre_account, - Account::default(), - "Found new private account with non default values" - ); - - // Compute initialization nullifier - let nullifier = Nullifier::for_account_initialization(npk); - (nullifier, DUMMY_COMMITMENT_HASH) - }, - |membership_proof| { - // Compute commitment set digest associated with provided auth path - let commitment_pre = Commitment::new(npk, pre_account); - let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); - - // Compute update nullifier - let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); - (nullifier, set_digest) - }, - ) + let commitment_pre = Commitment::new(npk, pre_account); + let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); + let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); + (nullifier, set_digest) } fn main() { let PrivacyPreservingCircuitInput { program_outputs, - visibility_mask, - private_account_keys, - private_account_nsks, - private_account_membership_proofs, + accounts, program_id, } = env::read(); - let execution_state = ExecutionState::derive_from_outputs( - &visibility_mask, - &private_account_keys, - program_id, - program_outputs, - ); + let execution_state = + ExecutionState::derive_from_outputs(&accounts, program_id, program_outputs); - let output = compute_circuit_output( - execution_state, - &visibility_mask, - &private_account_keys, - &private_account_nsks, - &private_account_membership_proofs, - ); + let output = compute_circuit_output(execution_state, &accounts); env::commit(&output); } diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 47037fbd..86ce745e 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -1075,7 +1075,7 @@ mod tests { program::Program, }; use nssa_core::{ - SharedSecretKey, + PrivacyPreservingCircuitInputAccount, SharedSecretKey, account::AccountWithMetadata, encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, }; @@ -1109,10 +1109,10 @@ mod tests { let (output, proof) = execute_and_prove( vec![AccountWithMetadata::new(Account::default(), true, &npk)], Program::serialize_instruction(0_u128).unwrap(), - vec![1], - vec![(npk, shared_secret)], - vec![nsk], - vec![None], + vec![PrivacyPreservingCircuitInputAccount::PrivateAuthorizedInit { + ssk: shared_secret, + nsk, + }], &Program::authenticated_transfer_program().into(), ) .unwrap(); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 460cfcfd..9c86c808 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -390,13 +390,7 @@ impl WalletCore { let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( pre_states, instruction_data, - acc_manager.visibility_mask().to_vec(), - private_account_keys - .iter() - .map(|keys| (keys.npk, keys.ssk)) - .collect::>(), - acc_manager.private_account_auth(), - acc_manager.private_account_membership_proofs(), + acc_manager.accounts(), &program.to_owned(), ) .unwrap(); diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 14a805c7..819fdc82 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -2,7 +2,8 @@ use anyhow::Result; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use nssa::{AccountId, PrivateKey}; use nssa_core::{ - MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInputAccount, + SharedSecretKey, account::{AccountWithMetadata, Nonce}, encryption::{EphemeralPublicKey, ViewingPublicKey}, }; @@ -51,7 +52,6 @@ enum State { pub struct AccountManager { states: Vec, - visibility_mask: Vec, } impl AccountManager { @@ -59,11 +59,10 @@ impl AccountManager { wallet: &WalletCore, accounts: Vec, ) -> Result { - let mut pre_states = Vec::with_capacity(accounts.len()); - let mut visibility_mask = Vec::with_capacity(accounts.len()); + let mut states = Vec::with_capacity(accounts.len()); for account in accounts { - let (state, mask) = match account { + let state = match account { PrivacyPreservingAccount::Public(account_id) => { let acc = wallet .get_account_public(account_id) @@ -73,37 +72,37 @@ impl AccountManager { let sk = wallet.get_account_public_signing_key(account_id).cloned(); let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); - (State::Public { account, sk }, 0) + State::Public { account, sk } } PrivacyPreservingAccount::PrivateOwned(account_id) => { let pre = private_acc_preparation(wallet, account_id).await?; - let mask = if pre.pre_state.is_authorized { 1 } else { 2 }; - (State::Private(pre), mask) + State::Private(pre) } PrivacyPreservingAccount::PrivateForeign { npk, vpk } => { let acc = nssa_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, &npk); + let eph_holder = EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); let pre = AccountPreparedData { nsk: None, npk, vpk, pre_state: auth_acc, proof: None, + ssk, + epk, }; - (State::Private(pre), 2) + State::Private(pre) } }; - pre_states.push(state); - visibility_mask.push(mask); + states.push(state); } - Ok(Self { - states: pre_states, - visibility_mask, - }) + Ok(Self { states }) } pub fn pre_states(&self) -> Vec { @@ -116,10 +115,6 @@ impl AccountManager { .collect() } - pub fn visibility_mask(&self) -> &[u8] { - &self.visibility_mask - } - pub fn public_account_nonces(&self) -> Vec { self.states .iter() @@ -134,37 +129,45 @@ impl AccountManager { self.states .iter() .filter_map(|state| match state { - State::Private(pre) => { - let eph_holder = EphemeralKeyHolder::new(&pre.npk); + State::Private(pre) => Some(PrivateAccountKeys { + npk: pre.npk, + ssk: pre.ssk, + vpk: pre.vpk.clone(), + epk: pre.epk.clone(), + }), + State::Public { .. } => None, + }) + .collect() + } - Some(PrivateAccountKeys { + /// Build the per-account input vec for the privacy-preserving circuit. Each variant carries + /// exactly the fields the circuit's code path for that account needs, with the ephemeral + /// keys (`ssk`) drawn from the cached values that `private_account_keys` and the message + /// construction also use, so all three views agree on the same ephemeral key. + pub fn accounts(&self) -> Vec { + self.states + .iter() + .map(|state| match state { + State::Public { .. } => PrivacyPreservingCircuitInputAccount::Public, + State::Private(pre) => match (pre.nsk, pre.proof.clone()) { + (Some(nsk), Some(membership_proof)) => { + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedUpdate { + ssk: pre.ssk, + nsk, + membership_proof, + } + } + (Some(nsk), None) => { + PrivacyPreservingCircuitInputAccount::PrivateAuthorizedInit { + ssk: pre.ssk, + nsk, + } + } + (None, _) => PrivacyPreservingCircuitInputAccount::PrivateUnauthorized { npk: pre.npk, - ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk), - vpk: pre.vpk.clone(), - epk: eph_holder.generate_ephemeral_public_key(), - }) - } - State::Public { .. } => None, - }) - .collect() - } - - pub fn private_account_auth(&self) -> Vec { - self.states - .iter() - .filter_map(|state| match state { - State::Private(pre) => pre.nsk, - State::Public { .. } => None, - }) - .collect() - } - - pub fn private_account_membership_proofs(&self) -> Vec> { - self.states - .iter() - .filter_map(|state| match state { - State::Private(pre) => Some(pre.proof.clone()), - State::Public { .. } => None, + ssk: pre.ssk, + }, + }, }) .collect() } @@ -196,6 +199,13 @@ struct AccountPreparedData { vpk: ViewingPublicKey, pre_state: AccountWithMetadata, proof: Option, + /// Cached shared-secret key derived once at `AccountManager::new`. Reused for both the + /// circuit input variant (`accounts()`) and the message ephemeral-key tuples + /// (`private_account_keys()`), so all consumers see the same key. The corresponding + /// `EphemeralKeyHolder` uses `OsRng` and would produce a different value on a second call. + ssk: SharedSecretKey, + /// Cached ephemeral public key, paired with `ssk`. + epk: EphemeralPublicKey, } async fn private_acc_preparation( @@ -226,11 +236,17 @@ async fn private_acc_preparation( // support from that in the wallet. let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, &from_npk); + let eph_holder = EphemeralKeyHolder::new(&from_npk); + let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk); + let epk = eph_holder.generate_ephemeral_public_key(); + Ok(AccountPreparedData { nsk: Some(nsk), npk: from_npk, vpk: from_vpk, pre_state: sender_pre, proof, + ssk, + epk, }) }