add root history

This commit is contained in:
Sergio Chouhy 2025-08-25 09:22:59 -03:00
parent 9ff6a5bc85
commit 20897596b0
8 changed files with 91 additions and 79 deletions

View File

@ -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<Account>,
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<Nullifier>,
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();

View File

@ -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<Account> = Vec::new();
let mut encrypted_private_post_states: Vec<EncryptedAccountData> = Vec::new();
let mut new_commitments: Vec<Commitment> = Vec::new();
let mut new_nullifiers: Vec<Nullifier> = 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);

View File

@ -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);
}

View File

@ -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]

View File

@ -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<u8> {
@ -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 {

View File

@ -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<Account>,
pub(crate) encrypted_private_post_states: Vec<EncryptedAccountData>,
pub(crate) new_commitments: Vec<Commitment>,
pub(crate) new_nullifiers: Vec<Nullifier>,
pub(crate) new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
}
impl Message {
@ -22,7 +22,7 @@ impl Message {
public_post_states: Vec<Account>,
encrypted_private_post_states: Vec<EncryptedAccountData>,
new_commitments: Vec<Commitment>,
new_nullifiers: Vec<Nullifier>,
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(),

View File

@ -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)

View File

@ -13,6 +13,7 @@ use std::collections::{HashMap, HashSet};
pub(crate) struct CommitmentSet {
merkle_tree: MerkleTree,
commitments: HashMap<Commitment, usize>,
pub root_history: HashSet<CommitmentSetDigest>,
}
impl CommitmentSet {
@ -22,9 +23,11 @@ impl CommitmentSet {
pub(crate) fn get_proof_for(&self, commitment: &Commitment) -> Option<MembershipProof> {
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<Nullifier>;
pub struct V01State {
public_state: HashMap<Address, Account>,
private_state: (CommitmentSet, NullifierSet),
pub private_state: (CommitmentSet, NullifierSet),
builtin_programs: HashMap<ProgramId, Program>,
}
@ -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::<Vec<Nullifier>>();
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();