From cee882502c72e85070fae9312f13332891bea4de Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 8 Sep 2025 19:29:56 -0300 Subject: [PATCH 01/19] wip --- nssa/core/src/account.rs | 4 +++- nssa/core/src/circuit_io.rs | 4 ++-- nssa/core/src/program.rs | 7 ++++--- .../privacy_preserving_transaction/circuit.rs | 18 ++++++++++++------ .../transaction.rs | 2 +- nssa/src/program.rs | 15 +++++++++------ nssa/src/public_transaction/transaction.rs | 2 +- nssa/src/state.rs | 12 ++++++------ 8 files changed, 38 insertions(+), 26 deletions(-) diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 688611e..1932de0 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -14,11 +14,13 @@ pub struct Account { pub nonce: Nonce, } +pub type FingerPrint = [u8; 32]; + #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct AccountWithMetadata { pub account: Account, - pub is_authorized: bool, + pub fingerprint: FingerPrint, } #[cfg(test)] diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index e619b2d..da989f0 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -56,7 +56,7 @@ mod tests { data: b"test data".to_vec(), nonce: 18446744073709551614, }, - is_authorized: true, + fingerprint: [0; 32], }, AccountWithMetadata { account: Account { @@ -65,7 +65,7 @@ mod tests { data: b"test data".to_vec(), nonce: 9999999999999999999999, }, - is_authorized: false, + fingerprint: [1; 32], }, ], public_post_states: vec![Account { diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index d284bbc..aa2684e 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,4 +1,4 @@ -use crate::account::{Account, AccountWithMetadata}; +use crate::account::{Account, AccountWithMetadata, FingerPrint}; use risc0_zkvm::serde::Deserializer; use risc0_zkvm::{DeserializeOwned, guest::env}; use serde::{Deserialize, Serialize}; @@ -21,8 +21,9 @@ pub struct ProgramOutput { pub fn read_nssa_inputs() -> ProgramInput { let pre_states: Vec = env::read(); - let words: InstructionData = env::read(); - let instruction = T::deserialize(&mut Deserializer::new(words.as_ref())).unwrap(); + let instruction_words: InstructionData = env::read(); + let authorized_fingerprints: Vec = env::read(); + let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap(); ProgramInput { pre_states, instruction, diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index ed32f98..e681c78 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -1,7 +1,7 @@ use nssa_core::{ MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, - account::AccountWithMetadata, + account::{AccountWithMetadata, FingerPrint}, program::{InstructionData, ProgramOutput}, }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; @@ -72,10 +72,16 @@ fn execute_and_prove_program( program: &Program, pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, + authorized_fingerprints: &[FingerPrint], ) -> Result { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); - Program::write_inputs(pre_states, instruction_data, &mut env_builder)?; + Program::write_inputs( + pre_states, + instruction_data, + authorized_fingerprints, + &mut env_builder, + )?; let env = env_builder.build().unwrap(); // Prove the program @@ -112,12 +118,12 @@ mod tests { balance: 100, ..Account::default() }, - is_authorized: true, + fingerprint: [0; 32], }; let recipient = AccountWithMetadata { account: Account::default(), - is_authorized: false, + fingerprint: [1; 32], }; let balance_to_move: u128 = 37; @@ -181,7 +187,7 @@ mod tests { nonce: 0xdeadbeef, ..Account::default() }, - is_authorized: true, + fingerprint: [0; 32], }; let sender_keys = test_private_account_keys_1(); let recipient_keys = test_private_account_keys_2(); @@ -189,7 +195,7 @@ mod tests { let recipient = AccountWithMetadata { account: Account::default(), - is_authorized: false, + fingerprint: [1; 32], }; let balance_to_move: u128 = 37; diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index 9aac54e..c782d01 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -92,7 +92,7 @@ impl PrivacyPreservingTransaction { .iter() .map(|address| AccountWithMetadata { account: state.get_account_by_address(address), - is_authorized: signer_addresses.contains(address), + fingerprint: *address.value(), }) .collect(); diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 66358e9..a40fdf9 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,5 +1,5 @@ use nssa_core::{ - account::{Account, AccountWithMetadata}, + account::{Account, AccountWithMetadata, FingerPrint}, program::{InstructionData, ProgramId, ProgramOutput}, }; use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID}; @@ -33,10 +33,11 @@ impl Program { &self, pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, + authorized_fingerprints: &[FingerPrint] ) -> Result, NssaError> { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); - Self::write_inputs(pre_states, instruction_data, &mut env_builder)?; + Self::write_inputs(pre_states, instruction_data, authorized_fingerprints, &mut env_builder)?; let env = env_builder.build().unwrap(); // Execute the program (without proving) @@ -58,11 +59,13 @@ impl Program { pub(crate) fn write_inputs( pre_states: &[AccountWithMetadata], instruction_data: &[u32], + authorized_fingerprints: &[FingerPrint], env_builder: &mut ExecutorEnvBuilder, ) -> Result<(), NssaError> { let pre_states = pre_states.to_vec(); + let authorized_fingerprints = authorized_fingerprints.to_vec(); env_builder - .write(&(pre_states, instruction_data)) + .write(&(pre_states, instruction_data, authorized_fingerprints)) .map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?; Ok(()) } @@ -173,11 +176,11 @@ mod tests { balance: 77665544332211, ..Account::default() }, - is_authorized: false, + fingerprint: [0; 32] }; let recipient = AccountWithMetadata { account: Account::default(), - is_authorized: false, + fingerprint: [1; 32] }; let expected_sender_post = Account { @@ -189,7 +192,7 @@ mod tests { ..Account::default() }; let [sender_post, recipient_post] = program - .execute(&[sender, recipient], &instruction_data) + .execute(&[sender, recipient], &instruction_data, &[]) .unwrap() .try_into() .unwrap(); diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 20b2729..bce7eaa 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -93,7 +93,7 @@ impl PublicTransaction { .iter() .map(|address| AccountWithMetadata { account: state.get_account_by_address(address), - is_authorized: signer_addresses.contains(address), + fingerprint: *address.value() }) .collect(); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index f980370..5ed4252 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -778,14 +778,14 @@ pub mod tests { ) -> PrivacyPreservingTransaction { let sender = AccountWithMetadata { account: state.get_account_by_address(&sender_keys.address()), - is_authorized: true, + fingerprint: *sender_keys.address().value(), }; let sender_nonce = sender.account.nonce; let recipient = AccountWithMetadata { account: Account::default(), - is_authorized: false, + fingerprint: recipient_keys.npk().to_byte_array(), }; let esk = [3; 32]; @@ -827,11 +827,11 @@ pub mod tests { let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); let sender_pre = AccountWithMetadata { account: sender_private_account.clone(), - is_authorized: true, + fingerprint: sender_keys.npk().to_byte_array(), }; let recipient_pre = AccountWithMetadata { account: Account::default(), - is_authorized: false, + fingerprint: recipient_keys.npk().to_byte_array(), }; let esk_1 = [3; 32]; @@ -887,11 +887,11 @@ pub mod tests { let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); let sender_pre = AccountWithMetadata { account: sender_private_account.clone(), - is_authorized: true, + fingerprint: sender_keys.npk().to_byte_array(), }; let recipient_pre = AccountWithMetadata { account: state.get_account_by_address(recipient_address), - is_authorized: false, + fingerprint: *recipient_address.value(), }; let esk = [3; 32]; From 80505f0440aeecc92c0acda433aea7a30bbcb4e4 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 9 Sep 2025 17:03:58 -0300 Subject: [PATCH 02/19] rollback to is_authorized field --- nssa/core/src/account.rs | 1 + nssa/core/src/circuit_io.rs | 2 ++ nssa/core/src/program.rs | 1 - .../src/privacy_preserving_transaction/circuit.rs | 6 ++++-- .../privacy_preserving_transaction/transaction.rs | 1 + nssa/src/program.rs | 15 +++++++-------- nssa/src/public_transaction/transaction.rs | 1 + nssa/src/state.rs | 6 ++++++ 8 files changed, 22 insertions(+), 11 deletions(-) diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 1932de0..94986c2 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -20,6 +20,7 @@ pub type FingerPrint = [u8; 32]; #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct AccountWithMetadata { pub account: Account, + pub is_authorized: bool, pub fingerprint: FingerPrint, } diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index da989f0..194b371 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -56,6 +56,7 @@ mod tests { data: b"test data".to_vec(), nonce: 18446744073709551614, }, + is_authorized: true, fingerprint: [0; 32], }, AccountWithMetadata { @@ -65,6 +66,7 @@ mod tests { data: b"test data".to_vec(), nonce: 9999999999999999999999, }, + is_authorized: false, fingerprint: [1; 32], }, ], diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index aa2684e..48593d8 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -22,7 +22,6 @@ pub struct ProgramOutput { pub fn read_nssa_inputs() -> ProgramInput { let pre_states: Vec = env::read(); let instruction_words: InstructionData = env::read(); - let authorized_fingerprints: Vec = env::read(); let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap(); ProgramInput { pre_states, diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index e681c78..ba7647c 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -72,14 +72,12 @@ fn execute_and_prove_program( program: &Program, pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, - authorized_fingerprints: &[FingerPrint], ) -> Result { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); Program::write_inputs( pre_states, instruction_data, - authorized_fingerprints, &mut env_builder, )?; let env = env_builder.build().unwrap(); @@ -118,11 +116,13 @@ mod tests { balance: 100, ..Account::default() }, + is_authorized: true, fingerprint: [0; 32], }; let recipient = AccountWithMetadata { account: Account::default(), + is_authorized: false, fingerprint: [1; 32], }; @@ -187,6 +187,7 @@ mod tests { nonce: 0xdeadbeef, ..Account::default() }, + is_authorized: true, fingerprint: [0; 32], }; let sender_keys = test_private_account_keys_1(); @@ -195,6 +196,7 @@ mod tests { let recipient = AccountWithMetadata { account: Account::default(), + is_authorized: false, fingerprint: [1; 32], }; let balance_to_move: u128 = 37; diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index c782d01..ee8eeba 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -92,6 +92,7 @@ impl PrivacyPreservingTransaction { .iter() .map(|address| AccountWithMetadata { account: state.get_account_by_address(address), + is_authorized: signer_addresses.contains(address), fingerprint: *address.value(), }) .collect(); diff --git a/nssa/src/program.rs b/nssa/src/program.rs index a40fdf9..0c05902 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -33,11 +33,10 @@ impl Program { &self, pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, - authorized_fingerprints: &[FingerPrint] ) -> Result, NssaError> { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); - Self::write_inputs(pre_states, instruction_data, authorized_fingerprints, &mut env_builder)?; + Self::write_inputs(pre_states, instruction_data, &mut env_builder)?; let env = env_builder.build().unwrap(); // Execute the program (without proving) @@ -59,13 +58,11 @@ impl Program { pub(crate) fn write_inputs( pre_states: &[AccountWithMetadata], instruction_data: &[u32], - authorized_fingerprints: &[FingerPrint], env_builder: &mut ExecutorEnvBuilder, ) -> Result<(), NssaError> { let pre_states = pre_states.to_vec(); - let authorized_fingerprints = authorized_fingerprints.to_vec(); env_builder - .write(&(pre_states, instruction_data, authorized_fingerprints)) + .write(&(pre_states, instruction_data)) .map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?; Ok(()) } @@ -176,11 +173,13 @@ mod tests { balance: 77665544332211, ..Account::default() }, - fingerprint: [0; 32] + is_authorized: true, + fingerprint: [0; 32], }; let recipient = AccountWithMetadata { account: Account::default(), - fingerprint: [1; 32] + is_authorized: false, + fingerprint: [1; 32], }; let expected_sender_post = Account { @@ -192,7 +191,7 @@ mod tests { ..Account::default() }; let [sender_post, recipient_post] = program - .execute(&[sender, recipient], &instruction_data, &[]) + .execute(&[sender, recipient], &instruction_data) .unwrap() .try_into() .unwrap(); diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index bce7eaa..f3a8ed6 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -93,6 +93,7 @@ impl PublicTransaction { .iter() .map(|address| AccountWithMetadata { account: state.get_account_by_address(address), + is_authorized: signer_addresses.contains(address), fingerprint: *address.value() }) .collect(); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 5ed4252..8662323 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -778,6 +778,7 @@ pub mod tests { ) -> PrivacyPreservingTransaction { let sender = AccountWithMetadata { account: state.get_account_by_address(&sender_keys.address()), + is_authorized: true, fingerprint: *sender_keys.address().value(), }; @@ -785,6 +786,7 @@ pub mod tests { let recipient = AccountWithMetadata { account: Account::default(), + is_authorized: false, fingerprint: recipient_keys.npk().to_byte_array(), }; @@ -827,10 +829,12 @@ pub mod tests { let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); let sender_pre = AccountWithMetadata { account: sender_private_account.clone(), + is_authorized: true, fingerprint: sender_keys.npk().to_byte_array(), }; let recipient_pre = AccountWithMetadata { account: Account::default(), + is_authorized: false, fingerprint: recipient_keys.npk().to_byte_array(), }; @@ -887,10 +891,12 @@ pub mod tests { let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); let sender_pre = AccountWithMetadata { account: sender_private_account.clone(), + is_authorized: true, fingerprint: sender_keys.npk().to_byte_array(), }; let recipient_pre = AccountWithMetadata { account: state.get_account_by_address(recipient_address), + is_authorized: false, fingerprint: *recipient_address.value(), }; From 3d240c72f8d325c00df20ceaeb6951258017f55f Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 10 Sep 2025 18:56:34 -0300 Subject: [PATCH 03/19] wip --- nssa/core/src/account.rs | 11 ++++++++-- nssa/core/src/circuit_io.rs | 7 +++---- nssa/core/src/nullifier.rs | 14 ++++++++++++- .../src/bin/privacy_preserving_circuit.rs | 11 +++++----- nssa/src/address.rs | 15 +++++++++++++ .../privacy_preserving_transaction/circuit.rs | 21 ++++++++----------- .../transaction.rs | 2 +- nssa/src/program.rs | 6 +++--- nssa/src/public_transaction/transaction.rs | 2 +- nssa/src/state.rs | 12 +++++------ 10 files changed, 65 insertions(+), 36 deletions(-) diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 94986c2..45e6e2c 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -1,4 +1,4 @@ -use crate::program::ProgramId; +use crate::{NullifierPublicKey, program::ProgramId}; use serde::{Deserialize, Serialize}; pub type Nonce = u128; @@ -14,7 +14,14 @@ pub struct Account { pub nonce: Nonce, } -pub type FingerPrint = [u8; 32]; +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[cfg_attr(any(feature = "host", test), derive(Debug))] +pub struct FingerPrint([u8; 32]); +impl FingerPrint { + pub fn new(value: [u8; 32]) -> Self { + Self(value) + } +} #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 194b371..2473475 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -40,8 +40,7 @@ impl PrivacyPreservingCircuitOutput { mod tests { use super::*; use crate::{ - Commitment, Nullifier, NullifierPublicKey, - account::{Account, AccountWithMetadata}, + account::{Account, AccountWithMetadata, FingerPrint}, Commitment, Nullifier, NullifierPublicKey }; use risc0_zkvm::serde::from_slice; @@ -57,7 +56,7 @@ mod tests { nonce: 18446744073709551614, }, is_authorized: true, - fingerprint: [0; 32], + fingerprint: FingerPrint::new([0; 32]), }, AccountWithMetadata { account: Account { @@ -67,7 +66,7 @@ mod tests { nonce: 9999999999999999999999, }, is_authorized: false, - fingerprint: [1; 32], + fingerprint: FingerPrint::new([1; 32]), }, ], public_post_states: vec![Account { diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index d1410de..c783091 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -1,12 +1,24 @@ use risc0_zkvm::sha::{Impl, Sha256}; use serde::{Deserialize, Serialize}; -use crate::Commitment; +use crate::{Commitment, account::FingerPrint}; #[derive(Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(any(feature = "host", test), derive(Debug, Clone, Hash))] pub struct NullifierPublicKey(pub(super) [u8; 32]); +impl From<&NullifierPublicKey> for FingerPrint { + fn from(value: &NullifierPublicKey) -> Self { + FingerPrint::new(value.0) + } +} + +impl From for FingerPrint { + fn from(value: NullifierPublicKey) -> Self { + FingerPrint::new(value.0) + } +} + impl From<&NullifierSecretKey> for NullifierPublicKey { fn from(value: &NullifierSecretKey) -> Self { let mut bytes = Vec::new(); 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 83f593a..346682d 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,12 +1,7 @@ use risc0_zkvm::{guest::env, serde::to_vec}; use nssa_core::{ - account::{Account, AccountWithMetadata}, - compute_digest_for_path, - encryption::Ciphertext, - program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, - Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, - PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + account::{Account, AccountWithMetadata, FingerPrint}, compute_digest_for_path, encryption::Ciphertext, program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput }; fn main() { @@ -70,6 +65,10 @@ fn main() { let new_nonce = private_nonces_iter.next().expect("Missing private nonce"); let (npk, shared_secret) = private_keys_iter.next().expect("Missing keys"); + if FingerPrint::from(npk) != pre_states[i].fingerprint { + panic!("Fingerprint mismatch"); + } + if visibility_mask[i] == 1 { // Private account with authentication let (nsk, membership_proof) = diff --git a/nssa/src/address.rs b/nssa/src/address.rs index 93304d5..0dba65a 100644 --- a/nssa/src/address.rs +++ b/nssa/src/address.rs @@ -1,5 +1,6 @@ use std::{fmt::Display, str::FromStr}; +use nssa_core::account::FingerPrint; use serde::{Deserialize, Serialize}; use crate::signature::PublicKey; @@ -81,6 +82,20 @@ impl<'de> Deserialize<'de> for Address { } } + +impl From<&Address> for FingerPrint { + fn from(address: &Address) -> Self { + FingerPrint::new(address.value) + } +} + +impl From
for FingerPrint { + fn from(address: Address) -> Self { + FingerPrint::new(address.value) + } +} + + #[cfg(test)] mod tests { use crate::{Address, address::AddressError}; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index ba7647c..c1afe39 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -75,11 +75,7 @@ fn execute_and_prove_program( ) -> Result { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); - Program::write_inputs( - pre_states, - instruction_data, - &mut env_builder, - )?; + Program::write_inputs(pre_states, instruction_data, &mut env_builder)?; let env = env_builder.build().unwrap(); // Prove the program @@ -110,6 +106,7 @@ mod tests { #[test] fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { + let recipient_keys = test_private_account_keys_1(); let program = Program::authenticated_transfer_program(); let sender = AccountWithMetadata { account: Account { @@ -117,13 +114,13 @@ mod tests { ..Account::default() }, is_authorized: true, - fingerprint: [0; 32], + fingerprint: FingerPrint::new([0; 32]), }; let recipient = AccountWithMetadata { account: Account::default(), is_authorized: false, - fingerprint: [1; 32], + fingerprint: recipient_keys.npk().into(), }; let balance_to_move: u128 = 37; @@ -143,7 +140,6 @@ mod tests { }; let expected_sender_pre = sender.clone(); - let recipient_keys = test_private_account_keys_1(); let esk = [3; 32]; let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.ivk()); @@ -181,6 +177,9 @@ mod tests { #[test] fn prove_privacy_preserving_execution_circuit_fully_private() { + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let sender_pre = AccountWithMetadata { account: Account { balance: 100, @@ -188,16 +187,14 @@ mod tests { ..Account::default() }, is_authorized: true, - fingerprint: [0; 32], + fingerprint: sender_keys.npk().into(), }; - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); let commitment_sender = Commitment::new(&sender_keys.npk(), &sender_pre.account); let recipient = AccountWithMetadata { account: Account::default(), is_authorized: false, - fingerprint: [1; 32], + fingerprint: recipient_keys.npk().into(), }; let balance_to_move: u128 = 37; diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index ee8eeba..a683f85 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -93,7 +93,7 @@ impl PrivacyPreservingTransaction { .map(|address| AccountWithMetadata { account: state.get_account_by_address(address), is_authorized: signer_addresses.contains(address), - fingerprint: *address.value(), + fingerprint: address.into(), }) .collect(); diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 0c05902..cf7c9da 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -77,7 +77,7 @@ impl Program { #[cfg(test)] mod tests { - use nssa_core::account::{Account, AccountWithMetadata}; + use nssa_core::account::{Account, AccountWithMetadata, FingerPrint}; use crate::program::Program; @@ -174,12 +174,12 @@ mod tests { ..Account::default() }, is_authorized: true, - fingerprint: [0; 32], + fingerprint: FingerPrint::new([0; 32]), }; let recipient = AccountWithMetadata { account: Account::default(), is_authorized: false, - fingerprint: [1; 32], + fingerprint: FingerPrint::new([1; 32]), }; let expected_sender_post = Account { diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index f3a8ed6..64e0707 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -94,7 +94,7 @@ impl PublicTransaction { .map(|address| AccountWithMetadata { account: state.get_account_by_address(address), is_authorized: signer_addresses.contains(address), - fingerprint: *address.value() + fingerprint: address.into() }) .collect(); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 8662323..347102a 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -779,7 +779,7 @@ pub mod tests { let sender = AccountWithMetadata { account: state.get_account_by_address(&sender_keys.address()), is_authorized: true, - fingerprint: *sender_keys.address().value(), + fingerprint: sender_keys.address().into(), }; let sender_nonce = sender.account.nonce; @@ -787,7 +787,7 @@ pub mod tests { let recipient = AccountWithMetadata { account: Account::default(), is_authorized: false, - fingerprint: recipient_keys.npk().to_byte_array(), + fingerprint: recipient_keys.npk().into(), }; let esk = [3; 32]; @@ -830,12 +830,12 @@ pub mod tests { let sender_pre = AccountWithMetadata { account: sender_private_account.clone(), is_authorized: true, - fingerprint: sender_keys.npk().to_byte_array(), + fingerprint: sender_keys.npk().into(), }; let recipient_pre = AccountWithMetadata { account: Account::default(), is_authorized: false, - fingerprint: recipient_keys.npk().to_byte_array(), + fingerprint: recipient_keys.npk().into(), }; let esk_1 = [3; 32]; @@ -892,12 +892,12 @@ pub mod tests { let sender_pre = AccountWithMetadata { account: sender_private_account.clone(), is_authorized: true, - fingerprint: sender_keys.npk().to_byte_array(), + fingerprint: sender_keys.npk().into(), }; let recipient_pre = AccountWithMetadata { account: state.get_account_by_address(recipient_address), is_authorized: false, - fingerprint: *recipient_address.value(), + fingerprint: recipient_address.into(), }; let esk = [3; 32]; From c3b2f4691bdc6a3dbcc71af706572038d42d86bb Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 11 Sep 2025 15:49:54 -0300 Subject: [PATCH 04/19] fmt, clippy --- nssa/core/src/account.rs | 2 +- nssa/core/src/circuit_io.rs | 7 ++++--- nssa/core/src/program.rs | 2 +- nssa/src/address.rs | 2 -- nssa/src/privacy_preserving_transaction/circuit.rs | 4 ++-- nssa/src/program.rs | 2 +- nssa/src/public_transaction/transaction.rs | 2 +- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 45e6e2c..c3d5580 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -1,4 +1,4 @@ -use crate::{NullifierPublicKey, program::ProgramId}; +use crate::program::ProgramId; use serde::{Deserialize, Serialize}; pub type Nonce = u128; diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 2473475..ecc25fc 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -40,7 +40,8 @@ impl PrivacyPreservingCircuitOutput { mod tests { use super::*; use crate::{ - account::{Account, AccountWithMetadata, FingerPrint}, Commitment, Nullifier, NullifierPublicKey + Commitment, Nullifier, NullifierPublicKey, + account::{Account, AccountWithMetadata, FingerPrint}, }; use risc0_zkvm::serde::from_slice; @@ -55,7 +56,7 @@ mod tests { data: b"test data".to_vec(), nonce: 18446744073709551614, }, - is_authorized: true, + is_authorized: true, fingerprint: FingerPrint::new([0; 32]), }, AccountWithMetadata { @@ -65,7 +66,7 @@ mod tests { data: b"test data".to_vec(), nonce: 9999999999999999999999, }, - is_authorized: false, + is_authorized: false, fingerprint: FingerPrint::new([1; 32]), }, ], diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 48593d8..a4e6722 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,4 +1,4 @@ -use crate::account::{Account, AccountWithMetadata, FingerPrint}; +use crate::account::{Account, AccountWithMetadata}; use risc0_zkvm::serde::Deserializer; use risc0_zkvm::{DeserializeOwned, guest::env}; use serde::{Deserialize, Serialize}; diff --git a/nssa/src/address.rs b/nssa/src/address.rs index 0dba65a..04bd99d 100644 --- a/nssa/src/address.rs +++ b/nssa/src/address.rs @@ -82,7 +82,6 @@ impl<'de> Deserialize<'de> for Address { } } - impl From<&Address> for FingerPrint { fn from(address: &Address) -> Self { FingerPrint::new(address.value) @@ -95,7 +94,6 @@ impl From
for FingerPrint { } } - #[cfg(test)] mod tests { use crate::{Address, address::AddressError}; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index c1afe39..82ba860 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -1,7 +1,7 @@ use nssa_core::{ MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, - account::{AccountWithMetadata, FingerPrint}, + account::AccountWithMetadata, program::{InstructionData, ProgramOutput}, }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; @@ -90,7 +90,7 @@ fn execute_and_prove_program( mod tests { use nssa_core::{ Commitment, EncryptionScheme, Nullifier, - account::{Account, AccountWithMetadata}, + account::{Account, AccountWithMetadata, FingerPrint}, }; use crate::{ diff --git a/nssa/src/program.rs b/nssa/src/program.rs index cf7c9da..d105f51 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,5 +1,5 @@ use nssa_core::{ - account::{Account, AccountWithMetadata, FingerPrint}, + account::{Account, AccountWithMetadata}, program::{InstructionData, ProgramId, ProgramOutput}, }; use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID}; diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 64e0707..14dcc6f 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -94,7 +94,7 @@ impl PublicTransaction { .map(|address| AccountWithMetadata { account: state.get_account_by_address(address), is_authorized: signer_addresses.contains(address), - fingerprint: address.into() + fingerprint: address.into(), }) .collect(); From 32910e76e3a58e247553571ba670d65de5561b0f Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 11 Sep 2025 16:37:28 -0300 Subject: [PATCH 05/19] refactor --- nssa/core/src/account.rs | 29 ++++++++++++ nssa/core/src/circuit_io.rs | 20 ++++---- nssa/core/src/nullifier.rs | 6 --- nssa/src/address.rs | 6 --- .../privacy_preserving_transaction/circuit.rs | 40 ++++++++-------- .../transaction.rs | 10 ++-- nssa/src/program.rs | 17 +++---- nssa/src/public_transaction/transaction.rs | 10 ++-- nssa/src/state.rs | 47 +++++++------------ 9 files changed, 95 insertions(+), 90 deletions(-) diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index c3d5580..54d0196 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -14,6 +14,8 @@ 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 FingerPrint([u8; 32]); @@ -31,6 +33,17 @@ pub struct AccountWithMetadata { pub fingerprint: FingerPrint, } +#[cfg(feature = "host")] +impl AccountWithMetadata { + pub fn new(account: Account, is_authorized: bool, fingerprint: impl Into) -> Self { + Self { + account, + is_authorized, + fingerprint: fingerprint.into(), + } + } +} + #[cfg(test)] mod tests { use crate::program::DEFAULT_PROGRAM_ID; @@ -64,4 +77,20 @@ mod tests { assert_eq!(new_acc.program_owner, DEFAULT_PROGRAM_ID); } + + #[test] + fn test_account_with_metadata_constructor() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: b"testing_account_with_metadata_constructor".to_vec(), + nonce: 0xdeadbeef, + }; + let fingerprint = FingerPrint::new([8; 32]); + let new_acc_with_metadata = + AccountWithMetadata::new(account.clone(), true, fingerprint.clone()); + assert_eq!(new_acc_with_metadata.account, account); + assert!(new_acc_with_metadata.is_authorized); + assert_eq!(new_acc_with_metadata.fingerprint, fingerprint); + } } diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index ecc25fc..beae76a 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -49,26 +49,26 @@ mod tests { fn test_privacy_preserving_circuit_output_to_bytes_is_compatible_with_from_slice() { let output = PrivacyPreservingCircuitOutput { public_pre_states: vec![ - AccountWithMetadata { - account: Account { + AccountWithMetadata::new( + Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 12345678901234567890, data: b"test data".to_vec(), nonce: 18446744073709551614, }, - is_authorized: true, - fingerprint: FingerPrint::new([0; 32]), - }, - AccountWithMetadata { - account: Account { + true, + FingerPrint::new([0; 32]), + ), + AccountWithMetadata::new( + Account { program_owner: [9, 9, 9, 8, 8, 8, 7, 7], balance: 123123123456456567112, data: b"test data".to_vec(), nonce: 9999999999999999999999, }, - is_authorized: false, - fingerprint: FingerPrint::new([1; 32]), - }, + false, + FingerPrint::new([1; 32]), + ), ], public_post_states: vec![Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index c783091..e852af4 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -13,12 +13,6 @@ impl From<&NullifierPublicKey> for FingerPrint { } } -impl From for FingerPrint { - fn from(value: NullifierPublicKey) -> Self { - FingerPrint::new(value.0) - } -} - impl From<&NullifierSecretKey> for NullifierPublicKey { fn from(value: &NullifierSecretKey) -> Self { let mut bytes = Vec::new(); diff --git a/nssa/src/address.rs b/nssa/src/address.rs index 04bd99d..f9d085e 100644 --- a/nssa/src/address.rs +++ b/nssa/src/address.rs @@ -88,12 +88,6 @@ impl From<&Address> for FingerPrint { } } -impl From
for FingerPrint { - fn from(address: Address) -> Self { - FingerPrint::new(address.value) - } -} - #[cfg(test)] mod tests { use crate::{Address, address::AddressError}; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 82ba860..281d59b 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -108,20 +108,20 @@ mod tests { fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { let recipient_keys = test_private_account_keys_1(); let program = Program::authenticated_transfer_program(); - let sender = AccountWithMetadata { - account: Account { + let sender = AccountWithMetadata::new( + Account { balance: 100, ..Account::default() }, - is_authorized: true, - fingerprint: FingerPrint::new([0; 32]), - }; + true, + FingerPrint::new([0; 32]), + ); - let recipient = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - fingerprint: recipient_keys.npk().into(), - }; + let recipient = AccountWithMetadata::new( + Account::default(), + false, + FingerPrint::from(&recipient_keys.npk()), + ); let balance_to_move: u128 = 37; @@ -180,22 +180,22 @@ mod tests { let sender_keys = test_private_account_keys_1(); let recipient_keys = test_private_account_keys_2(); - let sender_pre = AccountWithMetadata { - account: Account { + let sender_pre = AccountWithMetadata::new( + Account { balance: 100, nonce: 0xdeadbeef, ..Account::default() }, - is_authorized: true, - fingerprint: sender_keys.npk().into(), - }; + true, + FingerPrint::from(&sender_keys.npk()), + ); let commitment_sender = Commitment::new(&sender_keys.npk(), &sender_pre.account); - let recipient = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - fingerprint: recipient_keys.npk().into(), - }; + let recipient = AccountWithMetadata::new( + Account::default(), + false, + FingerPrint::from(&recipient_keys.npk()), + ); let balance_to_move: u128 = 37; let mut commitment_set = CommitmentSet::with_capacity(2); diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index a683f85..6992b2f 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -90,10 +90,12 @@ impl PrivacyPreservingTransaction { let public_pre_states: Vec<_> = message .public_addresses .iter() - .map(|address| AccountWithMetadata { - account: state.get_account_by_address(address), - is_authorized: signer_addresses.contains(address), - fingerprint: address.into(), + .map(|address| { + AccountWithMetadata::new( + state.get_account_by_address(address), + signer_addresses.contains(address), + address, + ) }) .collect(); diff --git a/nssa/src/program.rs b/nssa/src/program.rs index d105f51..552c436 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -168,19 +168,16 @@ mod tests { let program = Program::simple_balance_transfer(); let balance_to_move: u128 = 11223344556677; let instruction_data = Program::serialize_instruction(balance_to_move).unwrap(); - let sender = AccountWithMetadata { - account: Account { + let sender = AccountWithMetadata::new( + Account { balance: 77665544332211, ..Account::default() }, - is_authorized: true, - fingerprint: FingerPrint::new([0; 32]), - }; - let recipient = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - fingerprint: FingerPrint::new([1; 32]), - }; + true, + FingerPrint::new([0; 32]), + ); + let recipient = + AccountWithMetadata::new(Account::default(), false, FingerPrint::new([1; 32])); let expected_sender_post = Account { balance: 77665544332211 - balance_to_move, diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 14dcc6f..e5c8b5a 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -91,10 +91,12 @@ impl PublicTransaction { let pre_states: Vec<_> = message .addresses .iter() - .map(|address| AccountWithMetadata { - account: state.get_account_by_address(address), - is_authorized: signer_addresses.contains(address), - fingerprint: address.into(), + .map(|address| { + AccountWithMetadata::new( + state.get_account_by_address(address), + signer_addresses.contains(address), + address, + ) }) .collect(); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 347102a..4b8b25b 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -776,19 +776,15 @@ pub mod tests { balance_to_move: u128, state: &V01State, ) -> PrivacyPreservingTransaction { - let sender = AccountWithMetadata { - account: state.get_account_by_address(&sender_keys.address()), - is_authorized: true, - fingerprint: sender_keys.address().into(), - }; + let sender = AccountWithMetadata::new( + state.get_account_by_address(&sender_keys.address()), + true, + &sender_keys.address(), + ); let sender_nonce = sender.account.nonce; - let recipient = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - fingerprint: recipient_keys.npk().into(), - }; + let recipient = AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); let esk = [3; 32]; let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.ivk()); @@ -827,16 +823,10 @@ pub mod tests { ) -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); - let sender_pre = AccountWithMetadata { - account: sender_private_account.clone(), - is_authorized: true, - fingerprint: sender_keys.npk().into(), - }; - let recipient_pre = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - fingerprint: recipient_keys.npk().into(), - }; + let sender_pre = + AccountWithMetadata::new(sender_private_account.clone(), true, &sender_keys.npk()); + let recipient_pre = + AccountWithMetadata::new(Account::default(), false, &recipient_keys.npk()); let esk_1 = [3; 32]; let shared_secret_1 = SharedSecretKey::new(&esk_1, &sender_keys.ivk()); @@ -889,16 +879,13 @@ pub mod tests { ) -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); let sender_commitment = Commitment::new(&sender_keys.npk(), sender_private_account); - let sender_pre = AccountWithMetadata { - account: sender_private_account.clone(), - is_authorized: true, - fingerprint: sender_keys.npk().into(), - }; - let recipient_pre = AccountWithMetadata { - account: state.get_account_by_address(recipient_address), - is_authorized: false, - fingerprint: recipient_address.into(), - }; + let sender_pre = + AccountWithMetadata::new(sender_private_account.clone(), true, &sender_keys.npk()); + let recipient_pre = AccountWithMetadata::new( + state.get_account_by_address(recipient_address), + false, + recipient_address, + ); let esk = [3; 32]; let shared_secret = SharedSecretKey::new(&esk, &sender_keys.ivk()); From bfbb5e28709c1c6c5f4bd8e470e314bcafc61638 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Sep 2025 09:18:40 -0300 Subject: [PATCH 06/19] rename fingerprint to account_id --- nssa/core/src/account.rs | 14 +++++++------- nssa/core/src/circuit_io.rs | 6 +++--- nssa/core/src/nullifier.rs | 6 +++--- .../guest/src/bin/privacy_preserving_circuit.rs | 6 +++--- nssa/src/address.rs | 6 +++--- nssa/src/privacy_preserving_transaction/circuit.rs | 10 +++++----- nssa/src/program.rs | 6 +++--- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 54d0196..fdd51e1 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -18,8 +18,8 @@ pub struct 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 FingerPrint([u8; 32]); -impl FingerPrint { +pub struct AccountId([u8; 32]); +impl AccountId { pub fn new(value: [u8; 32]) -> Self { Self(value) } @@ -30,16 +30,16 @@ impl FingerPrint { pub struct AccountWithMetadata { pub account: Account, pub is_authorized: bool, - pub fingerprint: FingerPrint, + pub account_id: AccountId, } #[cfg(feature = "host")] impl AccountWithMetadata { - pub fn new(account: Account, is_authorized: bool, fingerprint: impl Into) -> Self { + pub fn new(account: Account, is_authorized: bool, account_id: impl Into) -> Self { Self { account, is_authorized, - fingerprint: fingerprint.into(), + account_id: account_id.into(), } } } @@ -86,11 +86,11 @@ mod tests { data: b"testing_account_with_metadata_constructor".to_vec(), nonce: 0xdeadbeef, }; - let fingerprint = FingerPrint::new([8; 32]); + let fingerprint = AccountId::new([8; 32]); let new_acc_with_metadata = AccountWithMetadata::new(account.clone(), true, fingerprint.clone()); assert_eq!(new_acc_with_metadata.account, account); assert!(new_acc_with_metadata.is_authorized); - assert_eq!(new_acc_with_metadata.fingerprint, fingerprint); + assert_eq!(new_acc_with_metadata.account_id, fingerprint); } } diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index beae76a..deeedbd 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -41,7 +41,7 @@ mod tests { use super::*; use crate::{ Commitment, Nullifier, NullifierPublicKey, - account::{Account, AccountWithMetadata, FingerPrint}, + account::{Account, AccountWithMetadata, AccountId}, }; use risc0_zkvm::serde::from_slice; @@ -57,7 +57,7 @@ mod tests { nonce: 18446744073709551614, }, true, - FingerPrint::new([0; 32]), + AccountId::new([0; 32]), ), AccountWithMetadata::new( Account { @@ -67,7 +67,7 @@ mod tests { nonce: 9999999999999999999999, }, false, - FingerPrint::new([1; 32]), + AccountId::new([1; 32]), ), ], public_post_states: vec![Account { diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index e852af4..405bb56 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -1,15 +1,15 @@ use risc0_zkvm::sha::{Impl, Sha256}; use serde::{Deserialize, Serialize}; -use crate::{Commitment, account::FingerPrint}; +use crate::{Commitment, account::AccountId}; #[derive(Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(any(feature = "host", test), derive(Debug, Clone, Hash))] pub struct NullifierPublicKey(pub(super) [u8; 32]); -impl From<&NullifierPublicKey> for FingerPrint { +impl From<&NullifierPublicKey> for AccountId { fn from(value: &NullifierPublicKey) -> Self { - FingerPrint::new(value.0) + AccountId::new(value.0) } } 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 346682d..398bfc6 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,7 +1,7 @@ use risc0_zkvm::{guest::env, serde::to_vec}; use nssa_core::{ - account::{Account, AccountWithMetadata, FingerPrint}, compute_digest_for_path, encryption::Ciphertext, program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput + account::{Account, AccountWithMetadata, AccountId}, compute_digest_for_path, encryption::Ciphertext, program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput }; fn main() { @@ -65,8 +65,8 @@ fn main() { let new_nonce = private_nonces_iter.next().expect("Missing private nonce"); let (npk, shared_secret) = private_keys_iter.next().expect("Missing keys"); - if FingerPrint::from(npk) != pre_states[i].fingerprint { - panic!("Fingerprint mismatch"); + if AccountId::from(npk) != pre_states[i].account_id { + panic!("AccountId mismatch"); } if visibility_mask[i] == 1 { diff --git a/nssa/src/address.rs b/nssa/src/address.rs index f9d085e..319b236 100644 --- a/nssa/src/address.rs +++ b/nssa/src/address.rs @@ -1,6 +1,6 @@ use std::{fmt::Display, str::FromStr}; -use nssa_core::account::FingerPrint; +use nssa_core::account::AccountId; use serde::{Deserialize, Serialize}; use crate::signature::PublicKey; @@ -82,9 +82,9 @@ impl<'de> Deserialize<'de> for Address { } } -impl From<&Address> for FingerPrint { +impl From<&Address> for AccountId { fn from(address: &Address) -> Self { - FingerPrint::new(address.value) + AccountId::new(address.value) } } diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 281d59b..3d6f594 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -90,7 +90,7 @@ fn execute_and_prove_program( mod tests { use nssa_core::{ Commitment, EncryptionScheme, Nullifier, - account::{Account, AccountWithMetadata, FingerPrint}, + account::{Account, AccountWithMetadata, AccountId}, }; use crate::{ @@ -114,13 +114,13 @@ mod tests { ..Account::default() }, true, - FingerPrint::new([0; 32]), + AccountId::new([0; 32]), ); let recipient = AccountWithMetadata::new( Account::default(), false, - FingerPrint::from(&recipient_keys.npk()), + AccountId::from(&recipient_keys.npk()), ); let balance_to_move: u128 = 37; @@ -187,14 +187,14 @@ mod tests { ..Account::default() }, true, - FingerPrint::from(&sender_keys.npk()), + AccountId::from(&sender_keys.npk()), ); let commitment_sender = Commitment::new(&sender_keys.npk(), &sender_pre.account); let recipient = AccountWithMetadata::new( Account::default(), false, - FingerPrint::from(&recipient_keys.npk()), + AccountId::from(&recipient_keys.npk()), ); let balance_to_move: u128 = 37; diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 552c436..6def822 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -77,7 +77,7 @@ impl Program { #[cfg(test)] mod tests { - use nssa_core::account::{Account, AccountWithMetadata, FingerPrint}; + use nssa_core::account::{Account, AccountWithMetadata, AccountId}; use crate::program::Program; @@ -174,10 +174,10 @@ mod tests { ..Account::default() }, true, - FingerPrint::new([0; 32]), + AccountId::new([0; 32]), ); let recipient = - AccountWithMetadata::new(Account::default(), false, FingerPrint::new([1; 32])); + AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); let expected_sender_post = Account { balance: 77665544332211 - balance_to_move, From 37b4d0d6e25241eba768de9972e180a1fac0499a Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Sep 2025 09:36:26 -0300 Subject: [PATCH 07/19] add tests --- nssa/core/src/nullifier.rs | 17 +++++++++++++++++ nssa/src/address.rs | 12 ++++++++++++ 2 files changed, 29 insertions(+) diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index 405bb56..c7ee26e 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -71,4 +71,21 @@ mod tests { let npk = NullifierPublicKey::from(&nsk); assert_eq!(npk, expected_npk); } + + #[test] + fn test_account_id_from_nullifier_public_key() { + let nsk = [ + 57, 5, 64, 115, 153, 56, 184, 51, 207, 238, 99, 165, 147, 214, 213, 151, 30, 251, 30, + 196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28, + ]; + let npk = NullifierPublicKey::from(&nsk); + let expected_account_id = AccountId::new([ + 202, 120, 42, 189, 194, 218, 78, 244, 31, 6, 108, 169, 29, 61, 22, 221, 69, 138, 197, + 161, 241, 39, 142, 242, 242, 50, 188, 201, 99, 28, 176, 238, + ]); + + let account_id = AccountId::from(&npk); + + assert_eq!(account_id, expected_account_id); + } } diff --git a/nssa/src/address.rs b/nssa/src/address.rs index 319b236..24fc7cf 100644 --- a/nssa/src/address.rs +++ b/nssa/src/address.rs @@ -90,6 +90,8 @@ impl From<&Address> for AccountId { #[cfg(test)] mod tests { + use nssa_core::account::AccountId; + use crate::{Address, address::AddressError}; #[test] @@ -119,4 +121,14 @@ mod tests { let result = hex_str.parse::
().unwrap_err(); assert!(matches!(result, AddressError::InvalidLength(_))); } + + #[test] + fn test_account_id_from_address() { + let address: Address = "37".repeat(32).parse().unwrap(); + let expected_account_id = AccountId::new([55; 32]); + + let account_id = AccountId::from(&address); + + assert_eq!(account_id, expected_account_id); + } } From 29914b3220e97ca0036241ecaebe69705450c27b Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Sep 2025 09:39:08 -0300 Subject: [PATCH 08/19] fmt --- nssa/core/src/circuit_io.rs | 2 +- nssa/src/address.rs | 2 +- nssa/src/privacy_preserving_transaction/circuit.rs | 2 +- nssa/src/program.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index deeedbd..14feef7 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -41,7 +41,7 @@ mod tests { use super::*; use crate::{ Commitment, Nullifier, NullifierPublicKey, - account::{Account, AccountWithMetadata, AccountId}, + account::{Account, AccountId, AccountWithMetadata}, }; use risc0_zkvm::serde::from_slice; diff --git a/nssa/src/address.rs b/nssa/src/address.rs index 24fc7cf..5837e20 100644 --- a/nssa/src/address.rs +++ b/nssa/src/address.rs @@ -126,7 +126,7 @@ mod tests { fn test_account_id_from_address() { let address: Address = "37".repeat(32).parse().unwrap(); let expected_account_id = AccountId::new([55; 32]); - + let account_id = AccountId::from(&address); assert_eq!(account_id, expected_account_id); diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 3d6f594..d8e5701 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -90,7 +90,7 @@ fn execute_and_prove_program( mod tests { use nssa_core::{ Commitment, EncryptionScheme, Nullifier, - account::{Account, AccountWithMetadata, AccountId}, + account::{Account, AccountId, AccountWithMetadata}, }; use crate::{ diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 6def822..1096df8 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -77,7 +77,7 @@ impl Program { #[cfg(test)] mod tests { - use nssa_core::account::{Account, AccountWithMetadata, AccountId}; + use nssa_core::account::{Account, AccountId, AccountWithMetadata}; use crate::program::Program; From b9e0ff230f4b988518d8ad01b7f907569b339bfb Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Sep 2025 15:18:25 -0300 Subject: [PATCH 09/19] add token program --- nssa/core/src/account.rs | 4 +- nssa/core/src/encoding.rs | 7 + nssa/program_methods/guest/Cargo.lock | 1 + nssa/program_methods/guest/Cargo.toml | 1 + nssa/program_methods/guest/src/bin/token.rs | 166 ++++++++++++++++++++ nssa/src/program.rs | 9 +- nssa/src/state.rs | 1 + 7 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 nssa/program_methods/guest/src/bin/token.rs diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index fdd51e1..597f558 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -2,7 +2,7 @@ use crate::program::ProgramId; use serde::{Deserialize, Serialize}; pub type Nonce = u128; -type Data = Vec; +pub type Data = Vec; /// Account to be used both in public and private contexts #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)] @@ -18,7 +18,7 @@ pub struct 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([u8; 32]); +pub struct AccountId(pub(super) [u8; 32]); impl AccountId { pub fn new(value: [u8; 32]) -> Self { Self(value) diff --git a/nssa/core/src/encoding.rs b/nssa/core/src/encoding.rs index dd586de..d7fc8b8 100644 --- a/nssa/core/src/encoding.rs +++ b/nssa/core/src/encoding.rs @@ -7,6 +7,7 @@ use std::io::Read; use crate::account::Account; +use crate::account::AccountId; #[cfg(feature = "host")] use crate::encryption::shared_key_derivation::Secp256k1Point; @@ -137,6 +138,12 @@ impl Secp256k1Point { } } +impl AccountId { + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nssa/program_methods/guest/Cargo.lock b/nssa/program_methods/guest/Cargo.lock index 18285e9..3330858 100644 --- a/nssa/program_methods/guest/Cargo.lock +++ b/nssa/program_methods/guest/Cargo.lock @@ -1817,6 +1817,7 @@ version = "0.1.0" dependencies = [ "nssa-core", "risc0-zkvm", + "serde", ] [[package]] diff --git a/nssa/program_methods/guest/Cargo.toml b/nssa/program_methods/guest/Cargo.toml index da4dbe8..59dac61 100644 --- a/nssa/program_methods/guest/Cargo.toml +++ b/nssa/program_methods/guest/Cargo.toml @@ -8,3 +8,4 @@ edition = "2024" [dependencies] risc0-zkvm = { version = "3.0.3", default-features = false, features = ['std'] } nssa-core = { path = "../../core" } +serde = { version = "1.0.219", default-features = false } diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs new file mode 100644 index 0000000..caa7692 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -0,0 +1,166 @@ +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +enum Instruction { + Transfer(u128), + NewDefinition([u8; 6], u128), +} + +const TOKEN_DEFINITION_TYPE: u8 = 0; +const TOKEN_DEFINITION_SIZE: usize = 23; + +const TOKEN_HOLDING_TYPE: u8 = 1; +const TOKEN_HOLDING_SIZE: usize = 49; + +struct TokenDefinition { + account_type: u8, + name: [u8; 6], + total_supply: u128, +} + +struct TokenHolding { + account_type: u8, + definition_id: AccountId, + balance: u128, +} + +impl TokenDefinition { + fn into_data(self) -> Vec { + let mut bytes = [0; TOKEN_DEFINITION_SIZE]; + bytes[0] = self.account_type; + bytes[1..7].copy_from_slice(&self.name); + bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); + bytes.into() + } +} + +impl TokenHolding { + fn new(definition_id: &AccountId) -> Self { + Self { + account_type: TOKEN_HOLDING_TYPE, + definition_id: definition_id.clone(), + balance: 0, + } + } + + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_HOLDING_SIZE && data[0] != TOKEN_HOLDING_TYPE { + None + } else { + let account_type = data[0]; + let definition_id = AccountId::new(data[33..65].try_into().unwrap()); + let balance = u128::from_le_bytes(data[65..].try_into().unwrap()); + Some(Self { + definition_id, + balance, + account_type, + }) + } + } + + fn into_data(self) -> Data { + let mut bytes = [0; TOKEN_HOLDING_SIZE]; + bytes[0] = self.account_type; + bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); + bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); + bytes.into() + } +} + +fn transfer(pre_states: Vec, balance_to_move: u128) { + let [sender, recipient] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let mut sender_holding = TokenHolding::parse(&sender.account.data).unwrap(); + let mut recipient_holding = if recipient.account == Account::default() { + TokenHolding::new(&sender_holding.definition_id) + } else { + TokenHolding::parse(&recipient.account.data).unwrap() + }; + + if sender_holding.definition_id != recipient_holding.definition_id { + panic!("Sender and recipient definition id mismatch"); + } + + if sender_holding.balance < balance_to_move { + panic!("Insufficient balance"); + } + + if !sender.is_authorized { + panic!("Sender authorization is missing"); + } + + sender_holding.balance -= balance_to_move; + recipient_holding.balance += balance_to_move; + + let sender_post = { + let mut this = sender.account.clone(); + this.data = sender_holding.into_data(); + this + }; + let recipient_post = { + let mut this = recipient.account.clone(); + this.data = recipient_holding.into_data(); + this + }; + + write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); +} + +fn new_definition(pre_states: Vec, name: [u8; 6], total_supply: u128) { + let [definition_target_account, holding_target_account] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + if definition_target_account.account != Account::default() { + panic!("Definition target account must have default values."); + } + + if holding_target_account.account != Account::default() { + panic!("Holding target account must have default values."); + } + + let token_definition = TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name, + total_supply, + }; + + let token_holding = TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: definition_target_account.account_id.clone(), + balance: total_supply, + }; + + let mut definition_target_account_post = definition_target_account.account.clone(); + definition_target_account_post.data = token_definition.into_data(); + + let mut holding_target_account_post = holding_target_account.account.clone(); + holding_target_account_post.data = token_holding.into_data(); + + write_nssa_outputs( + vec![definition_target_account, holding_target_account], + vec![definition_target_account_post, holding_target_account_post], + ); +} + +fn main() { + let ProgramInput { + pre_states, + instruction, + } = read_nssa_inputs::(); + + match instruction { + Instruction::Transfer(balance_to_move) => transfer(pre_states, balance_to_move), + Instruction::NewDefinition(name, total_supply) => { + new_definition(pre_states, name, total_supply) + } + }; +} diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 1096df8..9c34e1d 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -2,7 +2,7 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{InstructionData, ProgramId, ProgramOutput}, }; -use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID}; +use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, TOKEN_ELF, TOKEN_ID}; use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec}; use serde::Serialize; @@ -73,6 +73,13 @@ impl Program { elf: AUTHENTICATED_TRANSFER_ELF, } } + + pub fn token() -> Self { + Self { + id: TOKEN_ID, + elf: TOKEN_ELF, + } + } } #[cfg(test)] diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 4b8b25b..8f48888 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -86,6 +86,7 @@ impl V01State { }; this.insert_program(Program::authenticated_transfer_program()); + this.insert_program(Program::token()); this } From 856114019ef63c338dd0c636830feedf7c9005c1 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Sep 2025 17:13:37 -0300 Subject: [PATCH 10/19] fix --- nssa/core/src/program.rs | 4 +-- nssa/program_methods/guest/src/bin/token.rs | 32 ++++++++++++++------- nssa/src/lib.rs | 1 + nssa/src/state.rs | 21 ++++++++------ 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index a4e6722..233a0f7 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -71,8 +71,8 @@ pub fn validate_execution( // 5. Data changes only allowed if owned by executing program if pre.account.data != post.data - && (executing_program_id != pre.account.program_owner - || executing_program_id != post.program_owner) + && (pre.account != Account::default() && (executing_program_id != pre.account.program_owner + || executing_program_id != post.program_owner)) { return false; } diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index caa7692..49da9d7 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -4,11 +4,14 @@ use nssa_core::{ }; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] -enum Instruction { - Transfer(u128), - NewDefinition([u8; 6], u128), -} +// #[derive(Serialize, Deserialize)] +// enum Instruction { +// Transfer(u128), +// NewDefinition([u8; 6], u128), +// } +// +/// [type (1) || amount (16) || name (6)] +type Instruction = [u8; 23]; const TOKEN_DEFINITION_TYPE: u8 = 0; const TOKEN_DEFINITION_SIZE: usize = 23; @@ -52,8 +55,8 @@ impl TokenHolding { None } else { let account_type = data[0]; - let definition_id = AccountId::new(data[33..65].try_into().unwrap()); - let balance = u128::from_le_bytes(data[65..].try_into().unwrap()); + let definition_id = AccountId::new(data[1..33].try_into().unwrap()); + let balance = u128::from_le_bytes(data[33..].try_into().unwrap()); Some(Self { definition_id, balance, @@ -157,10 +160,19 @@ fn main() { instruction, } = read_nssa_inputs::(); - match instruction { - Instruction::Transfer(balance_to_move) => transfer(pre_states, balance_to_move), - Instruction::NewDefinition(name, total_supply) => { + match instruction[0] { + 0 => { + let total_supply = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); + let name: [u8; 6] = instruction[17..].try_into().unwrap(); + assert_ne!(name, [0; 6]); new_definition(pre_states, name, total_supply) } + 1 => { + let balance_to_move = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); + let name: [u8; 6] = instruction[17..].try_into().unwrap(); + assert_eq!(name, [0; 6]); + transfer(pre_states, balance_to_move) + } + _ => panic!("Invalid instruction"), }; } diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 25a9368..dd0df31 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -7,6 +7,7 @@ pub mod public_transaction; mod signature; mod state; +pub use nssa_core::account::Account; pub use address::Address; pub use nssa_core::account::Account; pub use privacy_preserving_transaction::{ diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 8f48888..42f9196 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -257,14 +257,14 @@ pub mod tests { let addr1 = Address::from(&PublicKey::new_from_private_key(&key1)); let addr2 = Address::from(&PublicKey::new_from_private_key(&key2)); let initial_data = [(addr1, 100u128), (addr2, 151u128)]; - let program = Program::authenticated_transfer_program(); + let authenticated_transfers_program = Program::authenticated_transfer_program(); let expected_public_state = { let mut this = HashMap::new(); this.insert( addr1, Account { balance: 100, - program_owner: program.id(), + program_owner: authenticated_transfers_program.id(), ..Account::default() }, ); @@ -272,7 +272,7 @@ pub mod tests { addr2, Account { balance: 151, - program_owner: program.id(), + program_owner: authenticated_transfers_program.id(), ..Account::default() }, ); @@ -280,7 +280,11 @@ pub mod tests { }; let expected_builtin_programs = { let mut this = HashMap::new(); - this.insert(program.id(), program); + this.insert( + authenticated_transfers_program.id(), + authenticated_transfers_program, + ); + this.insert(Program::token().id(), Program::token()); this }; @@ -660,12 +664,13 @@ pub mod tests { #[test] fn test_program_should_fail_if_modifies_data_of_non_owned_account() { let initial_data = []; - let mut state = V01State::new_with_genesis_accounts(&initial_data).with_test_programs(); - let address = Address::new([1; 32]); + let mut state = V01State::new_with_genesis_accounts(&initial_data) + .with_test_programs() + .with_non_default_accounts_but_default_program_owners(); + let address = Address::new([255; 32]); let program_id = Program::data_changer().id(); - // Consider the extreme case where the target account is the default account - assert_eq!(state.get_account_by_address(&address), Account::default()); + assert_ne!(state.get_account_by_address(&address), Account::default()); assert_ne!( state.get_account_by_address(&address).program_owner, program_id From afc977e0440c556f739f429ae66d0cb1b62c969a Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 15 Sep 2025 16:22:48 -0300 Subject: [PATCH 11/19] change validate_execution logic --- nssa/core/src/program.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 233a0f7..82023f3 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -59,20 +59,23 @@ pub fn validate_execution( return false; } - // 3. Ownership change only allowed from default accounts - if pre.account.program_owner != post.program_owner && pre.account != Account::default() { + // 3. Program ownership changes are not allowed + if pre.account.program_owner != post.program_owner { return false; } + let account_program_owner = pre.account.program_owner; + // 4. Decreasing balance only allowed if owned by executing program - if post.balance < pre.account.balance && pre.account.program_owner != executing_program_id { + if post.balance < pre.account.balance && account_program_owner != executing_program_id { return false; } - // 5. Data changes only allowed if owned by executing program + // 5. Data changes only allowed if owned by executing program or if account pre state has + // default values if pre.account.data != post.data - && (pre.account != Account::default() && (executing_program_id != pre.account.program_owner - || executing_program_id != post.program_owner)) + && pre.account != Account::default() + && account_program_owner != executing_program_id { return false; } From 24f3952c4951a527a0f69554180818e471729da0 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 15 Sep 2025 16:48:59 -0300 Subject: [PATCH 12/19] minor change --- nssa/program_methods/guest/src/bin/token.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 49da9d7..784e651 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -2,14 +2,7 @@ use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Data}, program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; -use serde::{Deserialize, Serialize}; -// #[derive(Serialize, Deserialize)] -// enum Instruction { -// Transfer(u128), -// NewDefinition([u8; 6], u128), -// } -// /// [type (1) || amount (16) || name (6)] type Instruction = [u8; 23]; From 5979d8d0cf36723dca0e6915b17735f5e3c09ee2 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 15 Sep 2025 16:49:14 -0300 Subject: [PATCH 13/19] fmt --- nssa/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index dd0df31..25a9368 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -7,7 +7,6 @@ pub mod public_transaction; mod signature; mod state; -pub use nssa_core::account::Account; pub use address::Address; pub use nssa_core::account::Account; pub use privacy_preserving_transaction::{ From 473a5fd98b25ccbc6ab55d351ef4697fc918a631 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 15 Sep 2025 18:09:28 -0300 Subject: [PATCH 14/19] add integratin tests wip --- integration_tests/src/lib.rs | 41 ++++++++ nssa/program_methods/guest/src/bin/token.rs | 29 ++++-- wallet/src/lib.rs | 110 ++++++++++++++++++++ 3 files changed, 173 insertions(+), 7 deletions(-) diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 5c14ea2..cdf92f0 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -172,6 +172,47 @@ pub async fn test_success_move_to_another_account() { info!("Success!"); } +pub async fn test_success_token_program() { + let wallet_config = fetch_config().unwrap(); + + wallet::execute_subcommand(Command::RegisterAccount {}) + .await + .unwrap(); + wallet::execute_subcommand(Command::RegisterAccount {}) + .await + .unwrap(); + wallet::execute_subcommand(Command::RegisterAccount {}) + .await + .unwrap(); + + let persistent_accounts = fetch_persistent_accounts().unwrap(); + + let mut new_persistent_accounts_addr = Vec::new(); + + for per_acc in persistent_accounts { + if (per_acc.address.to_string() != ACC_RECEIVER) + && (per_acc.address.to_string() != ACC_SENDER) + { + new_persistent_accounts_addr.push(per_acc.address.to_string()); + } + } + + let [definition_addr, supply_addr, other_addr] = new_persistent_accounts_addr + .try_into() + .expect("Failed to produce new account, not present in persistent accounts"); + + let command = Command::NewTokenDefinition { + definition_addr, + supply_addr, + name: "name".to_string(), + total_supply: 37, + }; + + wallet::execute_subcommand(command).await.unwrap(); + + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); +} + pub async fn test_failure() { let command = Command::SendNativeTokenTransfer { from: ACC_SENDER.to_string(), diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 784e651..a8c2a0a 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -3,14 +3,27 @@ use nssa_core::{ program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; -/// [type (1) || amount (16) || name (6)] -type Instruction = [u8; 23]; +// The token program has two functions: +// 1. New token definition. +// Arguments to this function are: +// * Two **default** accounts: [definition_account, holding_account]. +// The first default account will be populated with the token definition account values. The second account will +// be set to a token holding account for the new token, holding the entire total supply. +// * An instruction data of 23-bytes, indicating the total supply and the token name, with +// the following layout: +// [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] +// The name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] +// 2. Token transfer +// Arguments to this function are: +// * Two accounts: [sender_account, recipient_account]. +// * An instruction data byte string of length 23, indicating the total supply and the token name, with the following layout +// [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. const TOKEN_DEFINITION_TYPE: u8 = 0; -const TOKEN_DEFINITION_SIZE: usize = 23; +const TOKEN_DEFINITION_DATA_SIZE: usize = 23; const TOKEN_HOLDING_TYPE: u8 = 1; -const TOKEN_HOLDING_SIZE: usize = 49; +const TOKEN_HOLDING_DATA_SIZE: usize = 49; struct TokenDefinition { account_type: u8, @@ -26,7 +39,7 @@ struct TokenHolding { impl TokenDefinition { fn into_data(self) -> Vec { - let mut bytes = [0; TOKEN_DEFINITION_SIZE]; + let mut bytes = [0; TOKEN_DEFINITION_DATA_SIZE]; bytes[0] = self.account_type; bytes[1..7].copy_from_slice(&self.name); bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); @@ -44,7 +57,7 @@ impl TokenHolding { } fn parse(data: &[u8]) -> Option { - if data.len() != TOKEN_HOLDING_SIZE && data[0] != TOKEN_HOLDING_TYPE { + if data.len() != TOKEN_HOLDING_DATA_SIZE && data[0] != TOKEN_HOLDING_TYPE { None } else { let account_type = data[0]; @@ -59,7 +72,7 @@ impl TokenHolding { } fn into_data(self) -> Data { - let mut bytes = [0; TOKEN_HOLDING_SIZE]; + let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE]; bytes[0] = self.account_type; bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); @@ -147,6 +160,8 @@ fn new_definition(pre_states: Vec, name: [u8; 6], total_sup ); } +type Instruction = [u8; 23]; + fn main() { let ProgramInput { pre_states, diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 3bea629..4dbe8bf 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -126,6 +126,63 @@ impl WalletCore { } } + pub async fn send_new_token_definition( + &self, + definition_address: Address, + supply_address: Address, + name: [u8; 6], + total_supply: u128, + ) -> Result { + let addresses = vec![definition_address, supply_address]; + let program_id = nssa::program::Program::token().id(); + // Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] + let mut instruction = [0; 23]; + instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); + instruction[17..].copy_from_slice(&name); + let message = + nssa::public_transaction::Message::try_new(program_id, addresses, vec![], instruction) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.sequencer_client.send_tx(tx).await?) + } + + pub async fn send_token_transfer( + &self, + sender_address: Address, + recipient_address: Address, + amount: u128, + ) -> Result { + let addresses = vec![sender_address, recipient_address]; + let program_id = nssa::program::Program::token().id(); + // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. + let mut instruction = [0; 23]; + instruction[0] = 0x01; + instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + let Ok(nonces) = self.get_accounts_nonces(vec![sender_address]).await else { + return Err(ExecutionFailureKind::SequencerError); + }; + let message = + nssa::public_transaction::Message::try_new(program_id, addresses, nonces, instruction) + .unwrap(); + + let Some(signing_key) = self + .storage + .user_data + .get_account_signing_key(&sender_address) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.sequencer_client.send_tx(tx).await?) + } ///Get account balance pub async fn get_account_balance(&self, acc: Address) -> Result { Ok(self @@ -201,6 +258,24 @@ pub enum Command { #[arg(short, long)] addr: String, }, + NewTokenDefinition { + #[arg(short, long)] + definition_addr: String, + #[arg(short, long)] + supply_addr: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + total_supply: u128, + }, + TokenTransfer { + #[arg(short, long)] + sender_addr: String, + #[arg(short, long)] + recipient_addr: String, + #[arg(short, long)] + balance_to_move: u128, + }, } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config @@ -264,6 +339,41 @@ pub async fn execute_subcommand(command: Command) -> Result<()> { let account: HumanReadableAccount = wallet_core.get_account(addr).await?.into(); println!("{}", serde_json::to_string(&account).unwrap()); } + Command::NewTokenDefinition { + definition_addr, + supply_addr, + name, + total_supply, + } => { + let name = name.as_bytes(); + if name.len() > 6 { + // TODO: return error + panic!(); + } + let mut name_bytes = [0; 6]; + name_bytes[..name.len()].copy_from_slice(name); + wallet_core + .send_new_token_definition( + definition_addr.parse().unwrap(), + supply_addr.parse().unwrap(), + name_bytes, + total_supply, + ) + .await?; + } + Command::TokenTransfer { + sender_addr, + recipient_addr, + balance_to_move, + } => { + wallet_core + .send_token_transfer( + sender_addr.parse().unwrap(), + recipient_addr.parse().unwrap(), + balance_to_move, + ) + .await?; + } } Ok(()) From 9aab707ec7a4a22d82af8033c894400316d2f205 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 15 Sep 2025 22:28:39 -0300 Subject: [PATCH 15/19] finish test --- integration_tests/Cargo.toml | 1 + integration_tests/src/lib.rs | 186 +++++++++++++++++++++++++++-------- nssa/src/lib.rs | 2 +- wallet/src/lib.rs | 14 +-- 4 files changed, 155 insertions(+), 48 deletions(-) diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index a01935f..c3e2601 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -8,6 +8,7 @@ anyhow.workspace = true env_logger.workspace = true log.workspace = true actix.workspace = true +bytemuck = "1.23.2" actix-web.workspace = true tokio.workspace = true diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index cdf92f0..1646ee6 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -172,47 +172,6 @@ pub async fn test_success_move_to_another_account() { info!("Success!"); } -pub async fn test_success_token_program() { - let wallet_config = fetch_config().unwrap(); - - wallet::execute_subcommand(Command::RegisterAccount {}) - .await - .unwrap(); - wallet::execute_subcommand(Command::RegisterAccount {}) - .await - .unwrap(); - wallet::execute_subcommand(Command::RegisterAccount {}) - .await - .unwrap(); - - let persistent_accounts = fetch_persistent_accounts().unwrap(); - - let mut new_persistent_accounts_addr = Vec::new(); - - for per_acc in persistent_accounts { - if (per_acc.address.to_string() != ACC_RECEIVER) - && (per_acc.address.to_string() != ACC_SENDER) - { - new_persistent_accounts_addr.push(per_acc.address.to_string()); - } - } - - let [definition_addr, supply_addr, other_addr] = new_persistent_accounts_addr - .try_into() - .expect("Failed to produce new account, not present in persistent accounts"); - - let command = Command::NewTokenDefinition { - definition_addr, - supply_addr, - name: "name".to_string(), - total_supply: 37, - }; - - wallet::execute_subcommand(command).await.unwrap(); - - let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); -} - pub async fn test_failure() { let command = Command::SendNativeTokenTransfer { from: ACC_SENDER.to_string(), @@ -344,6 +303,147 @@ pub async fn test_get_account_wallet_command() { assert_eq!(account.nonce, 0); } +/// This test creates a new token using the token program. After creating the token, the test executes a +/// token transfer to a new account. +pub async fn test_success_token_program() { + let wallet_config = fetch_config().unwrap(); + + // Create new account for the token definition + wallet::execute_subcommand(Command::RegisterAccount {}) + .await + .unwrap(); + // Create new account for the token supply holder + wallet::execute_subcommand(Command::RegisterAccount {}) + .await + .unwrap(); + // Create new account for receiving a token transaction + wallet::execute_subcommand(Command::RegisterAccount {}) + .await + .unwrap(); + + let persistent_accounts = fetch_persistent_accounts().unwrap(); + + let mut new_persistent_accounts_addr = Vec::new(); + + for per_acc in persistent_accounts { + if (per_acc.address.to_string() != ACC_RECEIVER) + && (per_acc.address.to_string() != ACC_SENDER) + { + new_persistent_accounts_addr.push(per_acc.address); + } + } + + let [definition_addr, supply_addr, recipient_addr] = new_persistent_accounts_addr + .try_into() + .expect("Failed to produce new account, not present in persistent accounts"); + + // Create new token + let command = Command::CreateNewToken { + definition_addr: definition_addr.to_string(), + supply_addr: supply_addr.to_string(), + name: "A NAME".to_string(), + total_supply: 37, + }; + wallet::execute_subcommand(command).await.unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); + + // Check the status of the token definition account is the expected after the execution + let definition_acc = seq_client + .get_account(definition_addr.to_string()) + .await + .unwrap() + .account; + + assert_eq!(definition_acc.program_owner, Program::token().id()); + // The data of a token definition account has the following layout: + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ] + assert_eq!( + definition_acc.data, + vec![ + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + // Check the status of the token holding account with the total supply is the expected after the execution + let supply_acc = seq_client + .get_account(supply_addr.to_string()) + .await + .unwrap() + .account; + + // The account must be owned by the token program + assert_eq!(supply_acc.program_owner, Program::token().id()); + // The data of a token definition account has the following layout: + // [ 0x01 || corresponding_token_definition_id (32 bytes) || balance (little endian 16 bytes) ] + // First byte of the data equal to 1 means it's a token holding account + assert_eq!(supply_acc.data[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + // In this example, this is a token account of the newly created token, so it is expected + // to be equal to the address of the token definition account. + assert_eq!( + &supply_acc.data[1..33], + nssa::AccountId::from(&definition_addr).to_bytes() + ); + assert_eq!( + u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()), + 37 + ); + + // Transfer 7 tokens from `supply_acc` to the account at address `recipient_addr` + let command = Command::TransferToken { + sender_addr: supply_addr.to_string(), + recipient_addr: recipient_addr.to_string(), + balance_to_move: 7, + }; + wallet::execute_subcommand(command).await.unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Check the status of the account at `supply_addr` is the expected after the execution + let supply_acc = seq_client + .get_account(supply_addr.to_string()) + .await + .unwrap() + .account; + // The account must be owned by the token program + assert_eq!(supply_acc.program_owner, Program::token().id()); + // First byte equal to 1 means it's a token holding account + assert_eq!(supply_acc.data[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + assert_eq!( + &supply_acc.data[1..33], + nssa::AccountId::from(&definition_addr).to_bytes() + ); + assert_eq!( + u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()), + 30 + ); + + // Check the status of the account at `recipient_addr` is the expected after the execution + let recipient_acc = seq_client + .get_account(recipient_addr.to_string()) + .await + .unwrap() + .account; + + // The account must be owned by the token program + assert_eq!(recipient_acc.program_owner, Program::token().id()); + // First byte equal to 1 means it's a token holding account + assert_eq!(recipient_acc.data[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + assert_eq!( + &recipient_acc.data[1..33], + nssa::AccountId::from(&definition_addr).to_bytes() + ); + assert_eq!( + u128::from_le_bytes(recipient_acc.data[33..].try_into().unwrap()), + 7 + ); +} + macro_rules! test_cleanup_wrap { ($home_dir:ident, $test_func:ident) => {{ let res = pre_test($home_dir.clone()).await.unwrap(); @@ -367,6 +467,9 @@ pub async fn main_tests_runner() -> Result<()> { } = args; match test_name.as_str() { + "test_success_token_program" => { + test_cleanup_wrap!(home_dir, test_success_token_program); + } "test_success_move_to_another_account" => { test_cleanup_wrap!(home_dir, test_success_move_to_another_account); } @@ -388,6 +491,7 @@ pub async fn main_tests_runner() -> Result<()> { test_cleanup_wrap!(home_dir, test_failure); test_cleanup_wrap!(home_dir, test_success_two_transactions); test_cleanup_wrap!(home_dir, test_get_account_wallet_command); + test_cleanup_wrap!(home_dir, test_success_token_program); } _ => { anyhow::bail!("Unknown test name"); diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 25a9368..2a5682c 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -8,7 +8,7 @@ mod signature; mod state; pub use address::Address; -pub use nssa_core::account::Account; +pub use nssa_core::account::{Account, AccountId}; pub use privacy_preserving_transaction::{ PrivacyPreservingTransaction, circuit::execute_and_prove, }; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 4dbe8bf..1352251 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -150,7 +150,7 @@ impl WalletCore { Ok(self.sequencer_client.send_tx(tx).await?) } - pub async fn send_token_transfer( + pub async fn send_transfer_token_transaction( &self, sender_address: Address, recipient_address: Address, @@ -258,7 +258,8 @@ pub enum Command { #[arg(short, long)] addr: String, }, - NewTokenDefinition { + //Create a new token using the token program + CreateNewToken { #[arg(short, long)] definition_addr: String, #[arg(short, long)] @@ -268,7 +269,8 @@ pub enum Command { #[arg(short, long)] total_supply: u128, }, - TokenTransfer { + //Transfer tokens using the token program + TransferToken { #[arg(short, long)] sender_addr: String, #[arg(short, long)] @@ -339,7 +341,7 @@ pub async fn execute_subcommand(command: Command) -> Result<()> { let account: HumanReadableAccount = wallet_core.get_account(addr).await?.into(); println!("{}", serde_json::to_string(&account).unwrap()); } - Command::NewTokenDefinition { + Command::CreateNewToken { definition_addr, supply_addr, name, @@ -361,13 +363,13 @@ pub async fn execute_subcommand(command: Command) -> Result<()> { ) .await?; } - Command::TokenTransfer { + Command::TransferToken { sender_addr, recipient_addr, balance_to_move, } => { wallet_core - .send_token_transfer( + .send_transfer_token_transaction( sender_addr.parse().unwrap(), recipient_addr.parse().unwrap(), balance_to_move, From aded05f49302a95544d5ca837ba803b2386a7120 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 19 Sep 2025 09:46:25 -0300 Subject: [PATCH 16/19] fix token program logic and add explicit panic on balance overflow --- nssa/program_methods/guest/src/bin/token.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index a8c2a0a..98add24 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -7,8 +7,8 @@ use nssa_core::{ // 1. New token definition. // Arguments to this function are: // * Two **default** accounts: [definition_account, holding_account]. -// The first default account will be populated with the token definition account values. The second account will -// be set to a token holding account for the new token, holding the entire total supply. +// The first default account will be initialized with the token definition account values. The second account will +// be initialized to a token holding account for the new token, holding the entire total supply. // * An instruction data of 23-bytes, indicating the total supply and the token name, with // the following layout: // [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] @@ -16,7 +16,7 @@ use nssa_core::{ // 2. Token transfer // Arguments to this function are: // * Two accounts: [sender_account, recipient_account]. -// * An instruction data byte string of length 23, indicating the total supply and the token name, with the following layout +// * An instruction data byte string of length 23, indicating the total supply with the following layout // [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. const TOKEN_DEFINITION_TYPE: u8 = 0; @@ -57,7 +57,7 @@ impl TokenHolding { } fn parse(data: &[u8]) -> Option { - if data.len() != TOKEN_HOLDING_DATA_SIZE && data[0] != TOKEN_HOLDING_TYPE { + if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { None } else { let account_type = data[0]; @@ -106,7 +106,10 @@ fn transfer(pre_states: Vec, balance_to_move: u128) { } sender_holding.balance -= balance_to_move; - recipient_holding.balance += balance_to_move; + recipient_holding.balance = recipient_holding + .balance + .checked_add(balance_to_move) + .expect("Recipient balance overflow."); let sender_post = { let mut this = sender.account.clone(); From fcebd5f72696a5b30e5d9144448b4e6e18b177a2 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 19 Sep 2025 12:22:28 -0300 Subject: [PATCH 17/19] add tests --- nssa/program_methods/guest/src/bin/token.rs | 406 +++++++++++++++++++- 1 file changed, 385 insertions(+), 21 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 98add24..7011662 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -80,17 +80,19 @@ impl TokenHolding { } } -fn transfer(pre_states: Vec, balance_to_move: u128) { - let [sender, recipient] = match pre_states.try_into() { - Ok(array) => array, - Err(_) => return, - }; +fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of input accounts"); + } + let sender = &pre_states[0]; + let recipient = &pre_states[1]; - let mut sender_holding = TokenHolding::parse(&sender.account.data).unwrap(); + let mut sender_holding = + TokenHolding::parse(&sender.account.data).expect("Invalid sender data"); let mut recipient_holding = if recipient.account == Account::default() { TokenHolding::new(&sender_holding.definition_id) } else { - TokenHolding::parse(&recipient.account.data).unwrap() + TokenHolding::parse(&recipient.account.data).expect("Invalid recipient data") }; if sender_holding.definition_id != recipient_holding.definition_id { @@ -122,21 +124,26 @@ fn transfer(pre_states: Vec, balance_to_move: u128) { this }; - write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); + vec![sender_post, recipient_post] } -fn new_definition(pre_states: Vec, name: [u8; 6], total_supply: u128) { - let [definition_target_account, holding_target_account] = match pre_states.try_into() { - Ok(array) => array, - Err(_) => return, - }; +fn new_definition( + pre_states: &[AccountWithMetadata], + name: [u8; 6], + total_supply: u128, +) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of input accounts"); + } + let definition_target_account = &pre_states[0]; + let holding_target_account = &pre_states[1]; if definition_target_account.account != Account::default() { - panic!("Definition target account must have default values."); + panic!("Definition target account must have default values"); } if holding_target_account.account != Account::default() { - panic!("Holding target account must have default values."); + panic!("Holding target account must have default values"); } let token_definition = TokenDefinition { @@ -157,10 +164,7 @@ fn new_definition(pre_states: Vec, name: [u8; 6], total_sup let mut holding_target_account_post = holding_target_account.account.clone(); holding_target_account_post.data = token_holding.into_data(); - write_nssa_outputs( - vec![definition_target_account, holding_target_account], - vec![definition_target_account_post, holding_target_account_post], - ); + vec![definition_target_account_post, holding_target_account_post] } type Instruction = [u8; 23]; @@ -173,17 +177,377 @@ fn main() { match instruction[0] { 0 => { + // Parse instruction let total_supply = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); let name: [u8; 6] = instruction[17..].try_into().unwrap(); assert_ne!(name, [0; 6]); - new_definition(pre_states, name, total_supply) + + // Execute + let post_states = new_definition(&pre_states, name, total_supply); + write_nssa_outputs(pre_states, post_states); } 1 => { + // Parse instruction let balance_to_move = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); let name: [u8; 6] = instruction[17..].try_into().unwrap(); assert_eq!(name, [0; 6]); - transfer(pre_states, balance_to_move) + + // Execute + let post_states = transfer(&pre_states, balance_to_move); + write_nssa_outputs(pre_states, post_states); } _ => panic!("Invalid instruction"), }; } + +#[cfg(test)] +mod tests { + use nssa_core::account::{Account, AccountId, AccountWithMetadata}; + + use crate::{TOKEN_HOLDING_DATA_SIZE, new_definition, transfer}; + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_1() { + let pre_states = vec![AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }]; + let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + } + + #[should_panic(expected = "Definition target account must have default values")] + #[test] + fn test_new_definition_non_default_first_account_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + } + + #[should_panic(expected = "Holding target account must have default values")] + #[test] + fn test_new_definition_non_default_second_account_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + } + + #[test] + fn test_new_definition_with_valid_inputs_succeeds() { + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: AccountId::new([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]), + }, + AccountWithMetadata { + account: Account { + ..Account::default() + }, + is_authorized: false, + account_id: AccountId::new([2; 32]), + }, + ]; + + let post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + let [definition_account, holding_account] = post_states.try_into().ok().unwrap(); + assert_eq!( + definition_account.data, + vec![ + 0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 + ] + ); + assert_eq!( + holding_account.data, + vec![ + 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 + ] + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_transfer_with_invalid_number_of_accounts_1() { + let pre_states = vec![AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_transfer_with_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Invalid sender data")] + #[test] + fn test_transfer_invalid_instruction_type_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // First byte should be 0x01 for transfers + data: vec![0; TOKEN_HOLDING_DATA_SIZE], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Invalid sender data")] + #[test] + fn test_transfer_invalid_data_size_should_fail_1() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Invalid sender data")] + #[test] + fn test_transfer_invalid_data_size_should_fail_2() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` + data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Sender and recipient definition id mismatch")] + #[test] + fn test_transfer_with_different_definition_ids_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + data: vec![1; TOKEN_HOLDING_DATA_SIZE], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + data: vec![1] + .into_iter() + .chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1]) + .collect(), + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 10); + } + + #[should_panic(expected = "Insufficient balance")] + #[test] + fn test_transfer_with_insufficient_balance_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Account with balance 37 + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(37)) + .collect(), + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + data: vec![1; TOKEN_HOLDING_DATA_SIZE], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + // Attempt to transfer 38 tokens + let _post_states = transfer(&pre_states, 38); + } + + #[should_panic(expected = "Sender authorization is missing")] + #[test] + fn test_transfer_without_sender_authorization_should_fail() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Account with balance 37 + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(37)) + .collect(), + ..Account::default() + }, + is_authorized: false, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + data: vec![1; TOKEN_HOLDING_DATA_SIZE], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = transfer(&pre_states, 37); + } + + #[test] + fn test_transfer_with_valid_inputs_succeeds() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Account with balance 37 + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(37)) + .collect(), + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account { + // Account with balance 255 + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(255)) + .collect(), + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let post_states = transfer(&pre_states, 11); + let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); + assert_eq!( + sender_post.data, + vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + assert_eq!( + recipient_post.data, + vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + } +} From 2de7e49eeb93b7833b9b3489ded72135f360e780 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 19 Sep 2025 12:23:11 -0300 Subject: [PATCH 18/19] fmt --- .../guest/src/bin/authenticated_transfer.rs | 2 +- .../guest/src/bin/privacy_preserving_circuit.rs | 7 ++++++- nssa/program_methods/guest/src/bin/token.rs | 7 ++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index 9e7f399..928b695 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; /// A transfer of balance program. /// To be used both in public and private contexts. 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 398bfc6..24ef38e 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,7 +1,12 @@ use risc0_zkvm::{guest::env, serde::to_vec}; use nssa_core::{ - account::{Account, AccountWithMetadata, AccountId}, compute_digest_for_path, encryption::Ciphertext, program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput + Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, + PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + account::{Account, AccountId, AccountWithMetadata}, + compute_digest_for_path, + encryption::Ciphertext, + program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution}, }; fn main() { diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 7011662..e5680be 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -204,7 +204,7 @@ fn main() { mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; - use crate::{TOKEN_HOLDING_DATA_SIZE, new_definition, transfer}; + use crate::{new_definition, transfer, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE}; #[should_panic(expected = "Invalid number of input accounts")] #[test] @@ -358,11 +358,12 @@ mod tests { #[should_panic(expected = "Invalid sender data")] #[test] fn test_transfer_invalid_instruction_type_should_fail() { + let invalid_type = TOKEN_HOLDING_TYPE ^ 1; let pre_states = vec![ AccountWithMetadata { account: Account { - // First byte should be 0x01 for transfers - data: vec![0; TOKEN_HOLDING_DATA_SIZE], + // First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts + data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE], ..Account::default() }, is_authorized: true, From f5f5ab4ef190ff11419d63452b41ab7d707b4238 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 19 Sep 2025 12:39:19 -0300 Subject: [PATCH 19/19] add token program tests to ci tests --- ci_scripts/test-ubuntu.sh | 4 ++++ 1 file changed, 4 insertions(+) mode change 100644 => 100755 ci_scripts/test-ubuntu.sh diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh old mode 100644 new mode 100755 index 6a1710b..0055dad --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -5,7 +5,11 @@ curl -L https://risczero.com/install | bash source env.sh RISC0_DEV_MODE=1 cargo test --release + cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info cargo run $(pwd)/configs/debug all +cd .. + +cd nssa/program_methods/guest && cargo test --release