From 20897596b0b1158a2ca168f5fe73c8d7362e001a Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 25 Aug 2025 09:22:59 -0300 Subject: [PATCH] add root history --- nssa/core/src/lib.rs | 21 +++++---- .../src/bin/privacy_preserving_circuit.rs | 20 +++------ nssa/src/merkle_tree/mod.rs | 36 ++++++++-------- .../privacy_preserving_transaction/circuit.rs | 17 +++----- .../encoding.rs | 12 ++++-- .../privacy_preserving_transaction/message.rs | 8 ++-- .../transaction.rs | 13 ++---- nssa/src/state.rs | 43 ++++++++++++++----- 8 files changed, 91 insertions(+), 79 deletions(-) diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index ba57f63..e523f6f 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -38,11 +38,10 @@ pub mod error; pub type CommitmentSetDigest = [u8; 32]; pub type MembershipProof = (usize, Vec<[u8; 32]>); -pub fn verify_membership_proof( +pub fn compute_root_associated_to_path( commitment: &Commitment, proof: &MembershipProof, - digest: &CommitmentSetDigest, -) -> bool { +) -> CommitmentSetDigest { let value_bytes = commitment.to_byte_array(); let mut result: [u8; 32] = Impl::hash_bytes(&value_bytes) .as_bytes() @@ -64,7 +63,7 @@ pub fn verify_membership_proof( } level_index >>= 1; } - &result == digest + result } pub type EphemeralPublicKey = Secp256k1Point; @@ -243,7 +242,6 @@ pub struct PrivacyPreservingCircuitInput { )>, pub private_account_auth: Vec<(NullifierSecretKey, MembershipProof)>, pub program_id: ProgramId, - pub commitment_set_digest: CommitmentSetDigest, } #[derive(Serialize, Deserialize)] @@ -253,8 +251,7 @@ pub struct PrivacyPreservingCircuitOutput { pub public_post_states: Vec, pub encrypted_private_post_states: Vec, pub new_commitments: Vec, - pub new_nullifiers: Vec, - pub commitment_set_digest: CommitmentSetDigest, + pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, } #[cfg(feature = "host")] @@ -313,11 +310,13 @@ mod tests { &NullifierPublicKey::from(&[1; 32]), &Account::default(), )], - new_nullifiers: vec![Nullifier::new( - &Commitment::new(&NullifierPublicKey::from(&[2; 32]), &Account::default()), - &[1; 32], + new_nullifiers: vec![( + Nullifier::new( + &Commitment::new(&NullifierPublicKey::from(&[2; 32]), &Account::default()), + &[1; 32], + ), + [0xab; 32], )], - commitment_set_digest: [0xab; 32], }; let bytes = output.to_bytes(); let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap(); diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index e240b7d..51108af 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -2,8 +2,9 @@ use risc0_zkvm::{guest::env, serde::to_vec}; use nssa_core::{ account::{Account, AccountWithMetadata, Commitment, Nullifier, NullifierPublicKey}, + compute_root_associated_to_path, program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, - verify_membership_proof, EncryptedAccountData, EphemeralPublicKey, EphemeralSecretKey, + CommitmentSetDigest, EncryptedAccountData, EphemeralPublicKey, EphemeralSecretKey, IncomingViewingPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, }; @@ -15,7 +16,6 @@ fn main() { private_account_keys, private_account_auth, program_id, - commitment_set_digest, } = env::read(); // TODO: Check that `program_execution_proof` is one of the allowed built-in programs @@ -44,7 +44,7 @@ fn main() { let mut public_post_states: Vec = Vec::new(); let mut encrypted_private_post_states: Vec = Vec::new(); let mut new_commitments: Vec = Vec::new(); - let mut new_nullifiers: Vec = Vec::new(); + let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new(); let mut private_nonces_iter = private_account_nonces.iter(); let mut private_keys_iter = private_account_keys.iter(); @@ -80,15 +80,10 @@ fn main() { panic!("Npk mismatch"); } - // Verify pre-state commitment membership + // Compute commitment set digest associated with provided auth path let commitment_pre = Commitment::new(Npk, &pre_states[i].account); - if !verify_membership_proof( - &commitment_pre, - membership_proof, - &commitment_set_digest, - ) { - panic!("Membership proof invalid"); - } + let set_digest = + compute_root_associated_to_path(&commitment_pre, membership_proof); // Check pre_state authorization if !pre_states[i].is_authorized { @@ -97,7 +92,7 @@ fn main() { // Compute nullifier let nullifier = Nullifier::new(&commitment_pre, nsk); - new_nullifiers.push(nullifier); + new_nullifiers.push((nullifier, set_digest)); } else { if pre_states[i].account != Account::default() { panic!("Found new private account with non default values."); @@ -156,7 +151,6 @@ fn main() { encrypted_private_post_states, new_commitments, new_nullifiers, - commitment_set_digest, }; env::commit(&output); diff --git a/nssa/src/merkle_tree/mod.rs b/nssa/src/merkle_tree/mod.rs index 1b60f34..4b0b06f 100644 --- a/nssa/src/merkle_tree/mod.rs +++ b/nssa/src/merkle_tree/mod.rs @@ -173,7 +173,7 @@ fn verify_authentication_path(value: &Value, index: usize, path: &[Node], root: fn prev_power_of_two(x: usize) -> usize { if x == 0 { - return 0; // define as 0 + return 0; } 1 << (usize::BITS as usize - x.leading_zeros() as usize - 1) } @@ -314,10 +314,10 @@ mod tests { let expected_root = hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"); - tree.insert(values[0]); - tree.insert(values[1]); - tree.insert(values[2]); - tree.insert(values[3]); + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + assert_eq!(3, tree.insert(values[3])); assert_eq!(tree.root(), expected_root); } @@ -331,9 +331,9 @@ mod tests { let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); - tree.insert(values[0]); - tree.insert(values[1]); - tree.insert(values[2]); + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); assert_eq!(tree.root(), expected_root); } @@ -347,9 +347,9 @@ mod tests { let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); - tree.insert(values[0]); - tree.insert(values[1]); - tree.insert(values[2]); + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); assert_eq!(tree.root(), expected_root); } @@ -361,9 +361,9 @@ mod tests { let values = [[1; 32], [2; 32], [3; 32]]; let expected_tree = MerkleTree::new(&values); - tree.insert(values[0]); - tree.insert(values[1]); - tree.insert(values[2]); + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); assert_eq!(expected_tree, tree); } @@ -375,10 +375,10 @@ mod tests { let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; let expected_tree = MerkleTree::new(&values); - tree.insert(values[0]); - tree.insert(values[1]); - tree.insert(values[2]); - tree.insert(values[3]); + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + assert_eq!(3, tree.insert(values[3])); assert_eq!(expected_tree, tree); } diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 41e462a..031790a 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -33,7 +33,6 @@ pub fn execute_and_prove( )], private_account_auth: &[(NullifierSecretKey, MembershipProof)], program: &Program, - commitment_set_digest: &CommitmentSetDigest, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { let inner_receipt = execute_and_prove_program(program, pre_states, instruction_data)?; @@ -49,7 +48,6 @@ pub fn execute_and_prove( private_account_keys: private_account_keys.to_vec(), private_account_auth: private_account_auth.to_vec(), program_id: program.id(), - commitment_set_digest: *commitment_set_digest, }; // Prove circuit. @@ -157,7 +155,6 @@ mod tests { &[(recipient_keys.npk(), recipient_keys.ivk(), [3; 32])], &[], &Program::authenticated_transfer_program(), - &[99; 32], ) .unwrap(); @@ -169,7 +166,6 @@ mod tests { assert_eq!(sender_post, expected_sender_post); assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.new_nullifiers.len(), 0); - assert_eq!(output.commitment_set_digest, [99; 32]); assert_eq!(output.encrypted_private_post_states.len(), 1); let recipient_post = output.encrypted_private_post_states[0] @@ -199,7 +195,13 @@ mod tests { }; let balance_to_move: u128 = 37; - let expected_new_nullifiers = vec![Nullifier::new(&commitment_sender, &sender_keys.nsk)]; + let mut commitment_set = CommitmentSet::with_capacity(2); + commitment_set.extend(&[commitment_sender.clone()]); + + let expected_new_nullifiers = vec![( + Nullifier::new(&commitment_sender, &sender_keys.nsk), + commitment_set.digest(), + )]; let program = Program::authenticated_transfer_program(); @@ -220,9 +222,6 @@ mod tests { Commitment::new(&recipient_keys.npk(), &expected_private_account_2), ]; - let mut commitment_set = CommitmentSet::with_capacity(2); - commitment_set.extend(&[commitment_sender.clone()]); - let (output, proof) = execute_and_prove( &[sender_pre.clone(), recipient], &Program::serialize_instruction(balance_to_move).unwrap(), @@ -237,7 +236,6 @@ mod tests { commitment_set.get_proof_for(&commitment_sender).unwrap(), )], &program, - &commitment_set.digest(), ) .unwrap(); @@ -246,7 +244,6 @@ mod tests { assert!(output.public_post_states.is_empty()); assert_eq!(output.new_commitments, expected_new_commitments); assert_eq!(output.new_nullifiers, expected_new_nullifiers); - assert_eq!(output.commitment_set_digest, commitment_set.digest()); assert_eq!(output.encrypted_private_post_states.len(), 2); let recipient_post_1 = output.encrypted_private_post_states[0] diff --git a/nssa/src/privacy_preserving_transaction/encoding.rs b/nssa/src/privacy_preserving_transaction/encoding.rs index 3ff2953..0ab2db8 100644 --- a/nssa/src/privacy_preserving_transaction/encoding.rs +++ b/nssa/src/privacy_preserving_transaction/encoding.rs @@ -12,8 +12,7 @@ use crate::{Address, error::NssaError}; use super::message::Message; const MESSAGE_ENCODING_PREFIX_LEN: usize = 22; -const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = - b"\x01/NSSA/v0.1/TxMessage/"; +const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = b"\x01/NSSA/v0.1/TxMessage/"; impl Message { pub(crate) fn to_bytes(&self) -> Vec { @@ -56,9 +55,11 @@ impl Message { // New nullifiers let new_nullifiers_len: u32 = self.new_nullifiers.len() as u32; bytes.extend_from_slice(&new_nullifiers_len.to_le_bytes()); - for nullifier in &self.new_nullifiers { + for (nullifier, commitment_set_digest) in &self.new_nullifiers { bytes.extend_from_slice(&nullifier.to_byte_array()); + bytes.extend_from_slice(commitment_set_digest); } + bytes } @@ -125,7 +126,10 @@ impl Message { let new_nullifiers_len = u32::from_le_bytes(len_bytes) as usize; let mut new_nullifiers = Vec::with_capacity(new_nullifiers_len); for _ in 0..new_nullifiers_len { - new_nullifiers.push(Nullifier::from_cursor(cursor)?); + let nullifier = Nullifier::from_cursor(cursor)?; + let mut commitment_set_digest = [0; 32]; + cursor.read_exact(&mut commitment_set_digest); + new_nullifiers.push((nullifier, commitment_set_digest)); } Ok(Self { diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 4dddc2d..b4de4a7 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -1,5 +1,5 @@ use nssa_core::{ - EncryptedAccountData, + CommitmentSetDigest, EncryptedAccountData, account::{Account, Commitment, Nonce, Nullifier}, }; @@ -12,7 +12,7 @@ pub struct Message { pub(crate) public_post_states: Vec, pub(crate) encrypted_private_post_states: Vec, pub(crate) new_commitments: Vec, - pub(crate) new_nullifiers: Vec, + pub(crate) new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, } impl Message { @@ -22,7 +22,7 @@ impl Message { public_post_states: Vec, encrypted_private_post_states: Vec, new_commitments: Vec, - new_nullifiers: Vec, + new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, ) -> Self { Self { public_addresses, @@ -66,7 +66,7 @@ pub mod tests { let new_commitments = vec![Commitment::new(&Npk2, &account2)]; let old_commitment = Commitment::new(&Npk1, &account1); - let new_nullifiers = vec![Nullifier::new(&old_commitment, &nsk1)]; + let new_nullifiers = vec![(Nullifier::new(&old_commitment, &nsk1), [0; 32])]; Message { public_addresses: public_addresses.clone(), diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index 44dfaec..49f3d8d 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use nssa_core::account::{Account, AccountWithMetadata}; +use nssa_core::account::{Account, AccountWithMetadata, Commitment, Nullifier}; use nssa_core::{CommitmentSetDigest, EncryptedAccountData, PrivacyPreservingCircuitOutput}; use crate::error::NssaError; @@ -93,8 +93,6 @@ impl PrivacyPreservingTransaction { }) .collect(); - let set_commitment = state.commitment_set_digest(); - // 4. Proof verification check_privacy_preserving_circuit_proof_is_valid( &witness_set.proof, @@ -103,14 +101,13 @@ impl PrivacyPreservingTransaction { &message.encrypted_private_post_states, &message.new_commitments, &message.new_nullifiers, - set_commitment, )?; // 5. Commitment freshness state.check_commitments_are_new(&message.new_commitments)?; // 6. Nullifier uniqueness - state.check_nullifiers_are_new(&message.new_nullifiers)?; + state.check_nullifiers_are_valid(&message.new_nullifiers)?; Ok(message .public_addresses @@ -142,9 +139,8 @@ fn check_privacy_preserving_circuit_proof_is_valid( public_pre_states: &[AccountWithMetadata], public_post_states: &[Account], encrypted_private_post_states: &[EncryptedAccountData], - new_commitments: &[nssa_core::account::Commitment], - new_nullifiers: &[nssa_core::account::Nullifier], - commitment_set_digest: CommitmentSetDigest, + new_commitments: &[Commitment], + new_nullifiers: &[(Nullifier, CommitmentSetDigest)], ) -> Result<(), NssaError> { let output = PrivacyPreservingCircuitOutput { public_pre_states: public_pre_states.to_vec(), @@ -152,7 +148,6 @@ fn check_privacy_preserving_circuit_proof_is_valid( encrypted_private_post_states: encrypted_private_post_states.to_vec(), new_commitments: new_commitments.to_vec(), new_nullifiers: new_nullifiers.to_vec(), - commitment_set_digest, }; proof .is_valid_for(&output) diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 69af23d..3ac1e15 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -13,6 +13,7 @@ use std::collections::{HashMap, HashSet}; pub(crate) struct CommitmentSet { merkle_tree: MerkleTree, commitments: HashMap, + pub root_history: HashSet, } impl CommitmentSet { @@ -22,9 +23,11 @@ impl CommitmentSet { pub(crate) fn get_proof_for(&self, commitment: &Commitment) -> Option { let index = *self.commitments.get(commitment)?; - self.merkle_tree + let proof = self + .merkle_tree .get_authentication_path_for(index) - .map(|path| (index, path)) + .map(|path| (index, path)); + proof } pub(crate) fn extend(&mut self, commitments: &[Commitment]) { @@ -32,6 +35,7 @@ impl CommitmentSet { let index = self.merkle_tree.insert(commitment.to_byte_array()); self.commitments.insert(commitment, index); } + self.root_history.insert(self.digest()); } fn contains(&self, commitment: &Commitment) -> bool { @@ -42,6 +46,7 @@ impl CommitmentSet { Self { merkle_tree: MerkleTree::with_capacity(capacity), commitments: HashMap::new(), + root_history: HashSet::new(), } } } @@ -50,7 +55,7 @@ type NullifierSet = HashSet; pub struct V01State { public_state: HashMap, - private_state: (CommitmentSet, NullifierSet), + pub private_state: (CommitmentSet, NullifierSet), builtin_programs: HashMap, } @@ -122,7 +127,13 @@ impl V01State { self.private_state.0.extend(&message.new_commitments); // 3. Add new nullifiers - self.private_state.1.extend(message.new_nullifiers.clone()); + let new_nullifiers = message + .new_nullifiers + .iter() + .cloned() + .map(|(nullifier, _)| nullifier) + .collect::>(); + self.private_state.1.extend(new_nullifiers); // 4. Update public accounts for (address, post) in public_state_diff.into_iter() { @@ -172,19 +183,34 @@ impl V01State { Ok(()) } - pub(crate) fn check_nullifiers_are_new( + pub(crate) fn check_nullifiers_are_valid( &self, - new_nullifiers: &[Nullifier], + new_nullifiers: &[(Nullifier, CommitmentSetDigest)], ) -> Result<(), NssaError> { - for nullifier in new_nullifiers.iter() { + for (nullifier, digest) in new_nullifiers.iter() { if self.private_state.1.contains(nullifier) { return Err(NssaError::InvalidInput( "Nullifier already seen".to_string(), )); } + if !self.private_state.0.root_history.contains(digest) { + return Err(NssaError::InvalidInput( + "Unrecognized commitment set digest".to_string(), + )); + } } Ok(()) } + + pub(crate) fn check_commitment_set_digest_is_valid( + &self, + commitment_set_digest: &CommitmentSetDigest, + ) -> bool { + self.private_state + .0 + .root_history + .contains(commitment_set_digest) + } } #[cfg(test)] @@ -774,7 +800,6 @@ pub mod tests { &[(recipient_keys.npk(), recipient_keys.ivk(), esk)], &[], &Program::authenticated_transfer_program(), - &state.commitment_set_digest(), ) .unwrap(); @@ -828,7 +853,6 @@ pub mod tests { .unwrap(), )], &program, - &state.private_state.0.digest(), ) .unwrap(); @@ -880,7 +904,6 @@ pub mod tests { .unwrap(), )], &program, - &state.private_state.0.digest(), ) .unwrap();