diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index fbd2e66..f5e38b3 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -14,16 +14,6 @@ pub struct Account { pub nonce: Nonce, } -// /// A fingerprint of the owner of an account. This can be, for example, an `Address` in case the account -// /// is public, or a `NullifierPublicKey` in case the account is private. -// #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] -// #[cfg_attr(any(feature = "host", test), derive(Debug))] -// pub struct AccountId(pub(super) [u8; 32]); -// impl AccountId { -// pub fn new(value: [u8; 32]) -> Self { -// Self(value) -// } -// } pub type AccountId = Address; #[derive(Serialize, Deserialize, Clone)] diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 14feef7..6370dc6 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -82,7 +82,7 @@ mod tests { &Account::default(), )], new_nullifiers: vec![( - Nullifier::new( + Nullifier::for_account_update( &Commitment::new(&NullifierPublicKey::from(&[2; 32]), &Account::default()), &[1; 32], ), diff --git a/nssa/core/src/commitment.rs b/nssa/core/src/commitment.rs index 1eb8c98..c1f1cf7 100644 --- a/nssa/core/src/commitment.rs +++ b/nssa/core/src/commitment.rs @@ -7,9 +7,33 @@ use crate::{NullifierPublicKey, account::Account}; #[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq, Hash))] pub struct Commitment(pub(super) [u8; 32]); +/// A commitment to all zero data. +/// ```python +/// from hashlib import sha256 +/// hasher = sha256() +/// hasher.update(bytes([0] * 32 + [0] * 32 + [0] * 16 + [0] * 16 + list(sha256().digest()))) +/// DUMMY_COMMITMENT = hasher.digest() +/// ``` +pub const DUMMY_COMMITMENT: Commitment = Commitment([ + 130, 75, 48, 230, 171, 101, 121, 141, 159, 118, 21, 74, 135, 248, 16, 255, 238, 156, 61, 24, + 165, 33, 34, 172, 227, 30, 215, 20, 85, 47, 230, 29, +]); + +/// The hash of the dummy commitment +/// ```python +/// from hashlib import sha256 +/// hasher = sha256() +/// hasher.update(DUMMY_COMMITMENT) +/// DUMMY_COMMITMENT_HASH = hasher.digest() +/// ``` +pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [ + 170, 10, 217, 228, 20, 35, 189, 177, 238, 235, 97, 129, 132, 89, 96, 247, 86, 91, 222, 214, 38, + 194, 216, 67, 56, 251, 208, 226, 0, 117, 149, 39, +]; + impl Commitment { /// Generates the commitment to a private account owned by user for npk: - /// SHA256(npk || program_owner || balance || nonce || data) + /// SHA256(npk || program_owner || balance || nonce || SHA256(data)) pub fn new(npk: &NullifierPublicKey, account: &Account) -> Self { let mut bytes = Vec::new(); bytes.extend_from_slice(&npk.to_byte_array()); @@ -64,3 +88,30 @@ pub fn compute_digest_for_path( } result } + +#[cfg(test)] +mod tests { + use risc0_zkvm::sha::{Impl, Sha256}; + + use crate::{ + Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, NullifierPublicKey, account::Account, + }; + + #[test] + fn test_nothing_up_my_sleeve_dummy_commitment() { + let default_account = Account::default(); + let npk_null = NullifierPublicKey([0; 32]); + let expected_dummy_commitment = Commitment::new(&npk_null, &default_account); + assert_eq!(DUMMY_COMMITMENT, expected_dummy_commitment); + } + + #[test] + fn test_nothing_up_my_sleeve_dummy_commitment_hash() { + let expected_dummy_commitment_hash: [u8; 32] = + Impl::hash_bytes(&DUMMY_COMMITMENT.to_byte_array()) + .as_bytes() + .try_into() + .unwrap(); + assert_eq!(DUMMY_COMMITMENT_HASH, expected_dummy_commitment_hash); + } +} diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index 1aa63b4..d5c4aa3 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -9,7 +9,10 @@ pub mod program; pub mod address; pub use circuit_io::{PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput}; -pub use commitment::{Commitment, CommitmentSetDigest, MembershipProof, compute_digest_for_path}; +pub use commitment::{ + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, + compute_digest_for_path, +}; pub use encryption::{EncryptionScheme, SharedSecretKey}; pub use nullifier::{Nullifier, NullifierPublicKey, NullifierSecretKey}; diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index 200b3a4..5e3e208 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -45,12 +45,20 @@ pub type NullifierSecretKey = [u8; 32]; pub struct Nullifier(pub(super) [u8; 32]); impl Nullifier { - pub fn new(commitment: &Commitment, nsk: &NullifierSecretKey) -> Self { - let mut bytes = Vec::new(); + pub fn for_account_update(commitment: &Commitment, nsk: &NullifierSecretKey) -> Self { + const UPDATE_PREFIX: &[u8; 32] = b"/NSSA/v0.1/Nullifier/Update/\x00\x00\x00\x00"; + let mut bytes = UPDATE_PREFIX.to_vec(); bytes.extend_from_slice(&commitment.to_byte_array()); bytes.extend_from_slice(nsk); Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) } + + pub fn for_account_initialization(npk: &NullifierPublicKey) -> Self { + const INIT_PREFIX: &[u8; 32] = b"/NSSA/v0.1/Nullifier/Initialize/"; + let mut bytes = INIT_PREFIX.to_vec(); + bytes.extend_from_slice(&npk.to_byte_array()); + Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) + } } #[cfg(test)] @@ -58,14 +66,28 @@ mod tests { use super::*; #[test] - fn test_constructor() { + fn test_constructor_for_account_update() { let commitment = Commitment((0..32u8).collect::>().try_into().unwrap()); let nsk = [0x42; 32]; let expected_nullifier = Nullifier([ - 97, 87, 111, 191, 0, 44, 125, 145, 237, 104, 31, 230, 203, 254, 68, 176, 126, 17, 240, - 205, 249, 143, 11, 43, 15, 198, 189, 219, 191, 49, 36, 61, + 235, 128, 185, 229, 74, 74, 83, 13, 165, 48, 239, 24, 48, 101, 71, 251, 253, 92, 88, + 201, 103, 43, 250, 135, 193, 54, 175, 82, 245, 171, 90, 135, ]); - let nullifier = Nullifier::new(&commitment, &nsk); + let nullifier = Nullifier::for_account_update(&commitment, &nsk); + assert_eq!(nullifier, expected_nullifier); + } + + #[test] + fn test_constructor_for_account_initialization() { + let npk = NullifierPublicKey([ + 112, 188, 193, 129, 150, 55, 228, 67, 88, 168, 29, 151, 5, 92, 23, 190, 17, 162, 164, + 255, 29, 105, 42, 186, 43, 11, 157, 168, 132, 225, 17, 163, + ]); + let expected_nullifier = Nullifier([ + 96, 99, 33, 1, 116, 84, 169, 18, 85, 201, 17, 243, 123, 240, 242, 34, 116, 233, 92, + 203, 247, 92, 161, 162, 135, 66, 127, 108, 230, 149, 105, 157, + ]); + let nullifier = Nullifier::for_account_initialization(&npk); assert_eq!(nullifier, expected_nullifier); } 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 235b5c8..266db1a 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,8 +1,10 @@ +use std::collections::HashSet; + use risc0_zkvm::{guest::env, serde::to_vec}; use nssa_core::{ - Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, - PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, + Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, @@ -30,6 +32,11 @@ fn main() { post_states, } = program_output; + // Check that there are no repeated account ids + if !validate_uniqueness_of_account_ids(&pre_states) { + panic!("Repeated account ids found") + } + // Check that the program is well behaved. // See the # Programs section for the definition of the `validate_execution` method. if !validate_execution(&pre_states, &post_states, program_id) { @@ -98,8 +105,8 @@ fn main() { panic!("Pre-state not authorized"); } - // Compute nullifier - let nullifier = Nullifier::new(&commitment_pre, nsk); + // Compute update nullifier + let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); new_nullifiers.push((nullifier, set_digest)); } else { if pre_states[i].account != Account::default() { @@ -109,6 +116,10 @@ fn main() { if pre_states[i].is_authorized { panic!("Found new private account marked as authorized."); } + + // Compute initialization nullifier + let nullifier = Nullifier::for_account_initialization(npk); + new_nullifiers.push((nullifier, DUMMY_COMMITMENT_HASH)); } // Update post-state with new nonce @@ -161,3 +172,14 @@ fn main() { env::commit(&output); } + +fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool { + let number_of_accounts = pre_states.len(); + let number_of_account_ids = pre_states + .iter() + .map(|account| account.account_id.clone()) + .collect::>() + .len(); + + number_of_accounts == number_of_account_ids +} diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 8575bfe..3a98723 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -91,7 +91,7 @@ impl Proof { #[cfg(test)] mod tests { use nssa_core::{ - Commitment, EncryptionScheme, Nullifier, + Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, account::{Account, AccountId, AccountWithMetadata}, }; @@ -165,7 +165,7 @@ mod tests { assert_eq!(sender_pre, expected_sender_pre); assert_eq!(sender_post, expected_sender_post); assert_eq!(output.new_commitments.len(), 1); - assert_eq!(output.new_nullifiers.len(), 0); + assert_eq!(output.new_nullifiers.len(), 1); assert_eq!(output.ciphertexts.len(), 1); let recipient_post = EncryptionScheme::decrypt( @@ -206,10 +206,16 @@ mod tests { let mut commitment_set = CommitmentSet::with_capacity(2); commitment_set.extend(std::slice::from_ref(&commitment_sender)); - let expected_new_nullifiers = vec![( - Nullifier::new(&commitment_sender, &sender_keys.nsk), - commitment_set.digest(), - )]; + let expected_new_nullifiers = vec![ + ( + Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk), + commitment_set.digest(), + ), + ( + Nullifier::for_account_initialization(&recipient_keys.npk()), + DUMMY_COMMITMENT_HASH, + ), + ]; let program = Program::authenticated_transfer_program(); diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index d2227a8..0bcb02d 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -125,7 +125,10 @@ 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), [0; 32])]; + let new_nullifiers = vec![( + Nullifier::for_account_update(&old_commitment, &nsk1), + [0; 32], + )]; Message { public_addresses: public_addresses.clone(), diff --git a/nssa/src/state.rs b/nssa/src/state.rs index d9bff5a..3066809 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -4,7 +4,7 @@ use crate::{ public_transaction::PublicTransaction, }; use nssa_core::{ - Commitment, CommitmentSetDigest, MembershipProof, Nullifier, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, account::Account, address::Address, program::{DEFAULT_PROGRAM_ID, ProgramId}, @@ -84,6 +84,7 @@ impl V01State { .collect(); let mut private_state = CommitmentSet::with_capacity(32); + private_state.extend(&[DUMMY_COMMITMENT]); private_state.extend(initial_commitments); let mut this = Self { @@ -1026,7 +1027,8 @@ pub mod tests { ); let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); - let expected_new_nullifier = Nullifier::new(&sender_pre_commitment, &sender_keys.nsk); + let expected_new_nullifier = + Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); let expected_new_commitment_2 = Commitment::new( &recipient_keys.npk(), @@ -1100,7 +1102,8 @@ pub mod tests { ); let sender_pre_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); - let expected_new_nullifier = Nullifier::new(&sender_pre_commitment, &sender_keys.nsk); + let expected_new_nullifier = + Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); assert!(state.private_state.0.contains(&sender_pre_commitment)); assert!(!state.private_state.0.contains(&expected_new_commitment)); @@ -1398,10 +1401,10 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); // Setting only one nonce for an execution with two private accounts. let private_account_nonces = [0xdeadbeef1]; @@ -1438,7 +1441,7 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); @@ -1473,10 +1476,10 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); // Setting no auth key for an execution with one non default private accounts. let private_account_auth = []; @@ -1514,10 +1517,10 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); let private_account_keys = [ // First private account is the sender @@ -1562,7 +1565,7 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -1571,7 +1574,7 @@ pub mod tests { ..Account::default() }, false, - AccountId::new([1; 32]), + &recipient_keys.npk(), ); let result = execute_and_prove( @@ -1609,7 +1612,7 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -1618,7 +1621,7 @@ pub mod tests { ..Account::default() }, false, - AccountId::new([1; 32]), + &recipient_keys.npk(), ); let result = execute_and_prove( @@ -1655,7 +1658,7 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -1664,7 +1667,7 @@ pub mod tests { ..Account::default() }, false, - AccountId::new([1; 32]), + &recipient_keys.npk(), ); let result = execute_and_prove( @@ -1701,7 +1704,7 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -1710,7 +1713,7 @@ pub mod tests { ..Account::default() }, false, - AccountId::new([1; 32]), + &recipient_keys.npk(), ); let result = execute_and_prove( @@ -1748,13 +1751,13 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = AccountWithMetadata::new( Account::default(), // This should be set to false in normal circumstances true, - AccountId::new([1; 32]), + &recipient_keys.npk(), ); let result = execute_and_prove( @@ -1820,10 +1823,10 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); // Setting three new private account nonces for a circuit execution with only two private // accounts. @@ -1862,10 +1865,10 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); // Setting three private account keys for a circuit execution with only two private // accounts. @@ -1908,10 +1911,10 @@ pub mod tests { ..Account::default() }, true, - AccountId::new([0; 32]), + &sender_keys.npk(), ); let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + 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). @@ -1941,4 +1944,95 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } + + #[test] + fn test_private_accounts_can_only_be_initialized_once() { + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 100, + nonce: 0xdeadbeef, + data: vec![], + }; + let recipient_keys = test_private_account_keys_2(); + + let mut state = V01State::new_with_genesis_accounts(&[], &[]) + .with_private_account(&sender_keys, &sender_private_account); + + let balance_to_move = 37; + + let tx = private_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys, + balance_to_move, + [0xcafecafe, 0xfecafeca], + &state, + ); + + state + .transition_from_privacy_preserving_transaction(&tx) + .unwrap(); + + let sender_private_account = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 100 - balance_to_move, + nonce: 0xcafecafe, + data: vec![], + }; + + let tx = private_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys, + balance_to_move, + [0x1234, 0x5678], + &state, + ); + + let result = state.transition_from_privacy_preserving_transaction(&tx); + + assert!(matches!(result, Err(NssaError::InvalidInput(_)))); + let NssaError::InvalidInput(error_message) = result.err().unwrap() else { + panic!("Incorrect message error"); + }; + let expected_error_message = "Nullifier already seen".to_string(); + assert_eq!(error_message, expected_error_message); + } + + #[test] + fn test_circuit_should_fail_if_there_are_repeated_ids() { + 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 visibility_mask = [1, 1]; + let private_account_auth = [ + (sender_keys.nsk, (1, vec![])), + (sender_keys.nsk, (1, vec![])), + ]; + let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.ivk()); + let result = execute_and_prove( + &[private_account_1.clone(), private_account_1], + &Program::serialize_instruction(100u128).unwrap(), + &visibility_mask, + &[0xdeadbeef1, 0xdeadbeef2], + &[ + (sender_keys.npk(), shared_secret.clone()), + (sender_keys.npk(), shared_secret), + ], + &private_account_auth, + &program, + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } }