From dfd45c8678d78eed532827160d3b14d3a2a5cbab Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 6 Nov 2025 19:35:47 -0300 Subject: [PATCH 01/70] wip --- nssa/core/src/circuit_io.rs | 2 +- .../src/bin/privacy_preserving_circuit.rs | 77 +++++++++---------- .../privacy_preserving_transaction/circuit.rs | 31 +++++--- nssa/src/public_transaction/transaction.rs | 5 +- nssa/src/state.rs | 2 + 5 files changed, 63 insertions(+), 54 deletions(-) diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 6370dc6..197f0eb 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -10,7 +10,7 @@ use crate::{ #[derive(Serialize, Deserialize)] pub struct PrivacyPreservingCircuitInput { - pub program_output: ProgramOutput, + pub program_outputs: Vec, pub visibility_mask: Vec, pub private_account_nonces: Vec, pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, 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 d8ed15d..b7f70cf 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -3,17 +3,12 @@ use std::collections::HashSet; use risc0_zkvm::{guest::env, serde::to_vec}; use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, - Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, - account::{Account, AccountId, AccountWithMetadata}, - compute_digest_for_path, - encryption::Ciphertext, - program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution}, + account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, program::{validate_execution, ProgramId, ProgramOutput, DEFAULT_PROGRAM_ID}, Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, DUMMY_COMMITMENT_HASH }; fn main() { let PrivacyPreservingCircuitInput { - program_output, + program_outputs, visibility_mask, private_account_nonces, private_account_keys, @@ -21,9 +16,43 @@ fn main() { program_id, } = env::read(); - // Check that `program_output` is consistent with the execution of the corresponding program. - env::verify(program_id, &to_vec(&program_output).unwrap()).unwrap(); + // These lists will be the public outputs of this circuit + // and will be populated next. + let mut public_pre_states: Vec = Vec::new(); + let mut public_post_states: Vec = Vec::new(); + let mut ciphertexts: Vec = Vec::new(); + let mut new_commitments: Vec = Vec::new(); + let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new(); + for program_output in program_outputs.iter() { + // Check that `program_output` is consistent with the execution of the corresponding program. + env::verify(program_id, &to_vec(program_output).unwrap()).unwrap(); + } + + if private_nonces_iter.next().is_some() { + panic!("Too many nonces."); + } + + if private_keys_iter.next().is_some() { + panic!("Too many private account keys."); + } + + if private_auth_iter.next().is_some() { + panic!("Too many private account authentication keys."); + } + + let output = PrivacyPreservingCircuitOutput { + public_pre_states, + public_post_states, + ciphertexts, + new_commitments, + new_nullifiers, + }; + + env::commit(&output); +} + +fn validate_program_execution(program_output: &ProgramOutput, program_id: ProgramId) { let ProgramOutput { pre_states, post_states, @@ -51,14 +80,6 @@ fn main() { panic!("Invalid visibility mask length"); } - // These lists will be the public outputs of this circuit - // and will be populated next. - let mut public_pre_states: Vec = Vec::new(); - let mut public_post_states: Vec = Vec::new(); - let mut ciphertexts: Vec = Vec::new(); - let mut new_commitments: Vec = Vec::new(); - let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new(); - let mut private_nonces_iter = private_account_nonces.iter(); let mut private_keys_iter = private_account_keys.iter(); let mut private_auth_iter = private_account_auth.iter(); @@ -152,28 +173,6 @@ fn main() { _ => panic!("Invalid visibility mask value"), } } - - if private_nonces_iter.next().is_some() { - panic!("Too many nonces."); - } - - if private_keys_iter.next().is_some() { - panic!("Too many private account keys."); - } - - if private_auth_iter.next().is_some() { - panic!("Too many private account authentication keys."); - } - - let output = PrivacyPreservingCircuitOutput { - public_pre_states, - public_post_states, - ciphertexts, - new_commitments, - new_nullifiers, - }; - - env::commit(&output); } fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool { diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 9ce0610..7628e78 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -6,7 +6,7 @@ use nssa_core::{ }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; -use crate::{error::NssaError, program::Program}; +use crate::{error::NssaError, program::Program, state::MAX_NUMBER_CHAINED_CALLS}; use crate::program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}; @@ -25,15 +25,29 @@ pub fn execute_and_prove( private_account_auth: &[(NullifierSecretKey, MembershipProof)], program: &Program, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { - let inner_receipt = execute_and_prove_program(program, pre_states, instruction_data)?; + let mut env_builder = ExecutorEnv::builder(); + let mut program_outputs = Vec::new(); + for _i in 0..MAX_NUMBER_CHAINED_CALLS { + let inner_receipt = execute_and_prove_program(program, pre_states, instruction_data)?; - let program_output: ProgramOutput = inner_receipt - .journal - .decode() - .map_err(|e| NssaError::ProgramOutputDeserializationError(e.to_string()))?; + let program_output: ProgramOutput = inner_receipt + .journal + .decode() + .map_err(|e| NssaError::ProgramOutputDeserializationError(e.to_string()))?; + + // TODO: remove clone + program_outputs.push(program_output.clone()); + + // Prove circuit. + env_builder.add_assumption(inner_receipt); + + if program_output.chained_call.is_none() { + break; + } + } let circuit_input = PrivacyPreservingCircuitInput { - program_output, + program_outputs, visibility_mask: visibility_mask.to_vec(), private_account_nonces: private_account_nonces.to_vec(), private_account_keys: private_account_keys.to_vec(), @@ -41,9 +55,6 @@ pub fn execute_and_prove( program_id: program.id(), }; - // Prove circuit. - let mut env_builder = ExecutorEnv::builder(); - env_builder.add_assumption(inner_receipt); env_builder.write(&circuit_input).unwrap(); let env = env_builder.build().unwrap(); let prover = default_prover(); diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index d118d0c..27be904 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -8,9 +8,7 @@ use nssa_core::{ use sha2::{Digest, digest::FixedOutput}; use crate::{ - V02State, - error::NssaError, - public_transaction::{Message, WitnessSet}, + error::NssaError, public_transaction::{Message, WitnessSet}, state::MAX_NUMBER_CHAINED_CALLS, V02State }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -18,7 +16,6 @@ pub struct PublicTransaction { message: Message, witness_set: WitnessSet, } -const MAX_NUMBER_CHAINED_CALLS: usize = 10; impl PublicTransaction { pub fn new(message: Message, witness_set: WitnessSet) -> Self { diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 4120824..be3d0ab 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -10,6 +10,8 @@ use nssa_core::{ }; use std::collections::{HashMap, HashSet}; +pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; + pub(crate) struct CommitmentSet { merkle_tree: MerkleTree, commitments: HashMap, From ef7a5b66105e430b47ef5b4a485c85cfa2644e65 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 7 Nov 2025 20:42:00 -0300 Subject: [PATCH 02/70] wip --- nssa/core/src/program.rs | 1 + .../src/bin/privacy_preserving_circuit.rs | 91 ++++++++++++++----- nssa/src/public_transaction/transaction.rs | 6 +- 3 files changed, 74 insertions(+), 24 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index c7ceedc..3be22d4 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; pub type ProgramId = [u32; 8]; pub type InstructionData = Vec; pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; +pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; pub struct ProgramInput { pub pre_states: Vec, 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 bff975d..9efd77e 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,14 +1,14 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use risc0_zkvm::{guest::env, serde::to_vec}; use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, - Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, + NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, - program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution}, + program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramOutput, validate_execution}, }; fn main() { @@ -18,30 +18,77 @@ fn main() { private_account_nonces, private_account_keys, private_account_auth, - program_id, + mut program_id, } = env::read(); - // TODO: WIP - let program_output = program_outputs[0].clone(); + let mut pre_states: Vec = Vec::new(); + let mut state_diff: HashMap = HashMap::new(); - // Check that `program_output` is consistent with the execution of the corresponding program. - env::verify(program_id, &to_vec(&program_output).unwrap()).unwrap(); + let mut program_output = program_outputs[0].clone(); - let ProgramOutput { - pre_states, - post_states, - chained_call, - } = program_output; + for _i in 0..MAX_NUMBER_CHAINED_CALLS { + // Check that `program_output` is consistent with the execution of the corresponding program. + // TODO: Program output should contain the instruction data to verify the chain of call si + // performed correctly. + env::verify(program_id, &to_vec(&program_output).unwrap()).unwrap(); - // TODO: implement chained calls for privacy preserving transactions - if chained_call.is_some() { - panic!("Privacy preserving transactions do not support yet chained calls.") - } + // Check that the program is well behaved. + // See the # Programs section for the definition of the `validate_execution` method. + if !validate_execution( + &program_output.pre_states, + &program_output.post_states, + program_id, + ) { + panic!("Bad behaved program"); + } - // Check that the program is well behaved. - // See the # Programs section for the definition of the `validate_execution` method. - if !validate_execution(&pre_states, &post_states, program_id) { - panic!("Bad behaved program"); + // The invoked program claims the accounts with default program id. + for post in program_output.post_states.iter_mut() { + if post.program_owner == DEFAULT_PROGRAM_ID { + post.program_owner = program_id; + } + } + + for (pre, post) in program_output + .pre_states + .iter() + .zip(&program_output.post_states) + { + if !state_diff.contains_key(&pre.account_id) { + pre_states.push(pre.clone()); + } else { + state_diff.insert(pre.account_id, post.clone()); + } + } + + if let Some(next_chained_call) = program_output.chained_call { + program_id = next_chained_call.program_id; + + // // Build post states with metadata for next call + // let mut post_states_with_metadata = Vec::new(); + // for (pre, post) in program_output + // .pre_states + // .iter() + // .zip(program_output.post_states) + // { + // let mut post_with_metadata = pre.clone(); + // post_with_metadata.account = post.clone(); + // post_states_with_metadata.push(post_with_metadata); + // } + + // input_pre_states = next_chained_call + // .account_indices + // .iter() + // .map(|&i| { + // post_states_with_metadata + // .get(i) + // .ok_or_else(|| NssaError::InvalidInput("Invalid account indices".into())) + // .cloned() + // }) + // .collect::, NssaError>>()?; + } else { + break; + }; } let n_accounts = pre_states.len(); diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 27be904..6ff6c47 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -3,12 +3,14 @@ use std::collections::{HashMap, HashSet}; use nssa_core::{ account::{Account, AccountWithMetadata}, address::Address, - program::{DEFAULT_PROGRAM_ID, validate_execution}, + program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; use crate::{ - error::NssaError, public_transaction::{Message, WitnessSet}, state::MAX_NUMBER_CHAINED_CALLS, V02State + V02State, + error::NssaError, + public_transaction::{Message, WitnessSet}, }; #[derive(Debug, Clone, PartialEq, Eq)] From 6cbc5028cfa786a041e27177e65d7d4596778be6 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 11 Nov 2025 17:25:08 +0200 Subject: [PATCH 03/70] feat: tree construction --- .../src/key_management/key_tree/mod.rs | 17 +++++++++++++++++ wallet/src/lib.rs | 19 +++++++++++++++++++ wallet/src/main.rs | 5 ++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index dcc027b..7a25c81 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -127,6 +127,23 @@ impl KeyTree { self.addr_map.insert(addr, chain_index.clone()); self.key_map.insert(chain_index, node); } + + pub fn generate_tree_for_depth(&mut self, depth: u32) { + let mut id_stack = vec![ChainIndex::root()]; + + while !id_stack.is_empty() { + let curr_id = id_stack.pop().unwrap(); + + self.generate_new_node(curr_id.clone()); + + let mut next_id = curr_id.n_th_child(0); + + while (next_id.chain().iter().sum::()) < depth - 1 { + id_stack.push(next_id.clone()); + next_id = next_id.next_in_line(); + } + } + } } #[cfg(test)] diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 3b494a9..c39669f 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -259,6 +259,13 @@ pub enum OverCommand { #[arg(short, long)] password: String, }, + /// !!!WARNING!!! will rewrite current storage + RestoreKeys { + #[arg(short, long)] + password: String, + #[arg(short, long)] + depth: u32, + } } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config @@ -511,3 +518,15 @@ pub async fn execute_setup(password: String) -> Result<()> { Ok(()) } + +pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<()> { + let config = fetch_config().await?; + let mut wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password.clone()).await?; + + wallet_core.storage.user_data.public_key_tree.generate_tree_for_depth(depth); + wallet_core.storage.user_data.private_key_tree.generate_tree_for_depth(depth); + + wallet_core.store_persistent_data().await?; + + Ok(()) +} diff --git a/wallet/src/main.rs b/wallet/src/main.rs index 1fe52b3..986b27e 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -1,7 +1,7 @@ use anyhow::Result; use clap::{CommandFactory, Parser}; use tokio::runtime::Builder; -use wallet::{Args, OverCommand, execute_continious_run, execute_setup, execute_subcommand}; +use wallet::{Args, OverCommand, execute_continious_run, execute_keys_restoration, execute_setup, execute_subcommand}; pub const NUM_THREADS: usize = 2; @@ -25,6 +25,9 @@ fn main() -> Result<()> { OverCommand::Setup { password } => { execute_setup(password).await.unwrap(); } + OverCommand::RestoreKeys { password, depth } => { + execute_keys_restoration(password, depth).await.unwrap(); + } } } else if args.continious_run { execute_continious_run().await.unwrap(); From e92ad2132f769f60cd4ee7b885df0bf67fc2874e Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 12 Nov 2025 08:26:25 +0200 Subject: [PATCH 04/70] feat: keys restoration from mnemonic --- integration_tests/src/test_suite_map.rs | 131 ++++++++++++++++++ .../key_management/key_tree/chain_index.rs | 19 ++- .../src/key_management/key_tree/mod.rs | 73 +++++++++- wallet/src/lib.rs | 57 +++++++- wallet/src/main.rs | 5 +- 5 files changed, 271 insertions(+), 14 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index ff4ce84..4399407 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -3,6 +3,7 @@ use std::{ collections::HashMap, path::PathBuf, pin::Pin, + str::FromStr, time::{Duration, Instant}, }; @@ -1647,6 +1648,136 @@ pub fn prepare_function_map() -> HashMap { info!("Success!"); } + #[nssa_integration_test] + pub async fn test_keys_restoration() { + info!("########## test_keys_restoration ##########"); + let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); + + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: ChainIndex::root(), + })); + + let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + let SubcommandReturnValue::RegisterAccount { addr: to_addr1 } = sub_ret else { + panic!("FAILED TO REGISTER ACCOUNT"); + }; + + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: ChainIndex::from_str("/0").unwrap(), + })); + + let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + let SubcommandReturnValue::RegisterAccount { addr: to_addr2 } = sub_ret else { + panic!("FAILED TO REGISTER ACCOUNT"); + }; + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_private_account_input_from_str(&from.to_string()), + to: Some(make_private_account_input_from_str(&to_addr1.to_string())), + to_npk: None, + to_ipk: None, + amount: 100, + }); + + wallet::execute_subcommand(command).await.unwrap(); + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_private_account_input_from_str(&from.to_string()), + to: Some(make_private_account_input_from_str(&to_addr2.to_string())), + to_npk: None, + to_ipk: None, + amount: 100, + }); + + wallet::execute_subcommand(command).await.unwrap(); + + let from: Address = ACC_SENDER.parse().unwrap(); + + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: ChainIndex::root(), + })); + + let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + let SubcommandReturnValue::RegisterAccount { addr: to_addr3 } = sub_ret else { + panic!("FAILED TO REGISTER ACCOUNT"); + }; + + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: ChainIndex::from_str("/0").unwrap(), + })); + + let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + let SubcommandReturnValue::RegisterAccount { addr: to_addr4 } = sub_ret else { + panic!("FAILED TO REGISTER ACCOUNT"); + }; + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(&from.to_string()), + to: Some(make_public_account_input_from_str(&to_addr3.to_string())), + to_npk: None, + to_ipk: None, + amount: 100, + }); + + wallet::execute_subcommand(command).await.unwrap(); + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(&from.to_string()), + to: Some(make_public_account_input_from_str(&to_addr4.to_string())), + to_npk: None, + to_ipk: None, + amount: 100, + }); + + wallet::execute_subcommand(command).await.unwrap(); + + info!("########## PREPARATION END ##########"); + + wallet::execute_keys_restoration("test_pass".to_string(), 10) + .await + .unwrap(); + + let wallet_config = fetch_config().await.unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config.clone()) + .await + .unwrap(); + + assert!( + wallet_storage + .storage + .user_data + .private_key_tree + .get_node(to_addr1) + .is_some() + ); + assert!( + wallet_storage + .storage + .user_data + .private_key_tree + .get_node(to_addr2) + .is_some() + ); + assert!( + wallet_storage + .storage + .user_data + .public_key_tree + .get_node(to_addr3) + .is_some() + ); + assert!( + wallet_storage + .storage + .user_data + .public_key_tree + .get_node(to_addr4) + .is_some() + ); + + info!("Success!"); + } + println!("{function_map:#?}"); function_map diff --git a/key_protocol/src/key_management/key_tree/chain_index.rs b/key_protocol/src/key_management/key_tree/chain_index.rs index e22abf0..b7190d1 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -42,10 +42,13 @@ impl FromStr for ChainIndex { impl Display for ChainIndex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "/")?; - for cci in &self.0[..(self.0.len() - 1)] { - write!(f, "{cci}/")?; + if *self != Self::root() { + for cci in &self.0[..(self.0.len() - 1)] { + write!(f, "{cci}/")?; + } + write!(f, "{}", self.0.last().unwrap())?; } - write!(f, "{}", self.0.last().unwrap()) + Ok(()) } } @@ -74,6 +77,16 @@ impl ChainIndex { ChainIndex(chain) } + + pub fn depth(&self) -> u32 { + let mut res = 0; + + for cci in &self.0 { + res += cci + 1; + } + + res + } } #[cfg(test)] diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 7a25c81..d6600fe 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -1,5 +1,10 @@ -use std::collections::{BTreeMap, HashMap}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; +use anyhow::Result; +use common::sequencer_client::SequencerClient; use serde::{Deserialize, Serialize}; use crate::key_management::{ @@ -128,24 +133,80 @@ impl KeyTree { self.key_map.insert(chain_index, node); } + pub fn remove(&mut self, addr: nssa::Address) -> Option { + let chain_index = self.addr_map.remove(&addr).unwrap(); + self.key_map.remove(&chain_index) + } + pub fn generate_tree_for_depth(&mut self, depth: u32) { let mut id_stack = vec![ChainIndex::root()]; - while !id_stack.is_empty() { - let curr_id = id_stack.pop().unwrap(); - + while let Some(curr_id) = id_stack.pop() { self.generate_new_node(curr_id.clone()); let mut next_id = curr_id.n_th_child(0); - while (next_id.chain().iter().sum::()) < depth - 1 { + while (next_id.depth()) < depth - 1 { id_stack.push(next_id.clone()); next_id = next_id.next_in_line(); - } + } } } } +impl KeyTree { + pub fn cleanup_tree_for_depth(&mut self, depth: u32) { + let mut id_stack = vec![ChainIndex::root()]; + + while let Some(curr_id) = id_stack.pop() { + if let Some(node) = self.key_map.get(&curr_id) + && node.value.1 == nssa::Account::default() + && curr_id != ChainIndex::root() + { + let addr = node.address(); + self.remove(addr); + } + + let mut next_id = curr_id.n_th_child(0); + + while (next_id.depth()) < depth - 1 { + id_stack.push(next_id.clone()); + next_id = next_id.next_in_line(); + } + } + } +} + +impl KeyTree { + pub async fn cleanup_tree_for_depth( + &mut self, + depth: u32, + client: Arc, + ) -> Result<()> { + let mut id_stack = vec![ChainIndex::root()]; + + while let Some(curr_id) = id_stack.pop() { + if let Some(node) = self.key_map.get(&curr_id) { + let address = node.address(); + let node_acc = client.get_account(address.to_string()).await?.account; + + if node_acc == nssa::Account::default() && curr_id != ChainIndex::root() { + self.remove(address); + } + } + + let mut next_id = curr_id.n_th_child(0); + + while (next_id.depth()) < depth - 1 { + id_stack.push(next_id.clone()); + next_id = next_id.next_in_line(); + } + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index c39669f..8949e7b 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -265,7 +265,7 @@ pub enum OverCommand { password: String, #[arg(short, long)] depth: u32, - } + }, } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config @@ -521,10 +521,59 @@ pub async fn execute_setup(password: String) -> Result<()> { pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<()> { let config = fetch_config().await?; - let mut wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password.clone()).await?; + let mut wallet_core = + WalletCore::start_from_config_new_storage(config.clone(), password.clone()).await?; - wallet_core.storage.user_data.public_key_tree.generate_tree_for_depth(depth); - wallet_core.storage.user_data.private_key_tree.generate_tree_for_depth(depth); + wallet_core + .storage + .user_data + .public_key_tree + .generate_tree_for_depth(depth); + + println!("Public tree generated"); + + wallet_core + .storage + .user_data + .private_key_tree + .generate_tree_for_depth(depth); + + println!("Private tree generated"); + + wallet_core + .storage + .user_data + .public_key_tree + .cleanup_tree_for_depth(depth, wallet_core.sequencer_client.clone()) + .await?; + + println!("Public tree cleaned up"); + + let last_block = wallet_core + .sequencer_client + .get_last_block() + .await? + .last_block; + + println!("Last block is {last_block}"); + + parse_block_range( + 1, + last_block, + wallet_core.sequencer_client.clone(), + &mut wallet_core, + ) + .await?; + + println!("Private tree clean up start"); + + wallet_core + .storage + .user_data + .private_key_tree + .cleanup_tree_for_depth(depth); + + println!("Private tree cleaned up"); wallet_core.store_persistent_data().await?; diff --git a/wallet/src/main.rs b/wallet/src/main.rs index 986b27e..1a8b35a 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -1,7 +1,10 @@ use anyhow::Result; use clap::{CommandFactory, Parser}; use tokio::runtime::Builder; -use wallet::{Args, OverCommand, execute_continious_run, execute_keys_restoration, execute_setup, execute_subcommand}; +use wallet::{ + Args, OverCommand, execute_continious_run, execute_keys_restoration, execute_setup, + execute_subcommand, +}; pub const NUM_THREADS: usize = 2; From 38490a6163002f9a8d42c70afab8fe115e112fc7 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 18 Nov 2025 00:04:53 -0300 Subject: [PATCH 05/70] wip --- .../guest/src/bin/privacy_preserving_circuit.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 9efd77e..223229f 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -56,12 +56,11 @@ fn main() { { if !state_diff.contains_key(&pre.account_id) { pre_states.push(pre.clone()); - } else { - state_diff.insert(pre.account_id, post.clone()); } + state_diff.insert(pre.account_id.clone(), post.clone()); } - if let Some(next_chained_call) = program_output.chained_call { + if let Some(next_chained_call) = &program_output.chained_call { program_id = next_chained_call.program_id; // // Build post states with metadata for next call @@ -115,7 +114,7 @@ fn main() { // Public account public_pre_states.push(pre_states[i].clone()); - let mut post = post_states[i].clone(); + let mut post = state_diff.get(&pre_states[i].account_id).unwrap().clone(); if pre_states[i].is_authorized { post.nonce += 1; } @@ -171,7 +170,8 @@ fn main() { } // Update post-state with new nonce - let mut post_with_updated_values = post_states[i].clone(); + let mut post_with_updated_values = + state_diff.get(&pre_states[i].account_id).unwrap().clone(); post_with_updated_values.nonce = *new_nonce; if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID { From 4f650e939f61a976b403430a6eba8f813eb30ad6 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 18 Nov 2025 01:38:47 -0300 Subject: [PATCH 06/70] add instruction to the program output --- nssa/core/src/program.rs | 23 ++++++-- .../guest/src/bin/authenticated_transfer.rs | 42 +++++++++----- nssa/program_methods/guest/src/bin/pinata.rs | 22 +++++-- .../src/bin/privacy_preserving_circuit.rs | 57 +++++++++---------- nssa/program_methods/guest/src/bin/token.rs | 17 +++--- .../privacy_preserving_transaction/circuit.rs | 1 + .../guest/src/bin/burner.rs | 15 +++-- .../guest/src/bin/chain_caller.rs | 12 ++-- .../guest/src/bin/data_changer.rs | 6 +- .../guest/src/bin/extra_output.rs | 10 +++- .../guest/src/bin/minter.rs | 6 +- .../guest/src/bin/missing_output.rs | 6 +- .../guest/src/bin/nonce_changer.rs | 4 +- .../guest/src/bin/program_owner_changer.rs | 6 +- .../guest/src/bin/simple_balance_transfer.rs | 14 +++-- 15 files changed, 146 insertions(+), 95 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 3be22d4..7f6038f 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -26,23 +26,32 @@ pub struct ChainedCall { #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ProgramOutput { + pub instruction_data: InstructionData, pub pre_states: Vec, pub post_states: Vec, pub chained_call: Option, } -pub fn read_nssa_inputs() -> ProgramInput { +pub fn read_nssa_inputs() -> (ProgramInput, InstructionData) { let pre_states: Vec = env::read(); let instruction_words: InstructionData = env::read(); let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap(); - ProgramInput { - pre_states, - instruction, - } + ( + ProgramInput { + pre_states, + instruction, + }, + instruction_words, + ) } -pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec) { +pub fn write_nssa_outputs( + instruction_data: InstructionData, + pre_states: Vec, + post_states: Vec, +) { let output = ProgramOutput { + instruction_data, pre_states, post_states, chained_call: None, @@ -51,11 +60,13 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec } pub fn write_nssa_outputs_with_chained_call( + instruction_data: InstructionData, pre_states: Vec, post_states: Vec, chained_call: Option, ) { let output = ProgramOutput { + instruction_data, pre_states, post_states, chained_call, diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index df8a38e..2e05492 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -2,37 +2,42 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; +use risc0_zkvm::serde::to_vec; /// Initializes a default account under the ownership of this program. /// This is achieved by a noop. -fn initialize_account(pre_state: AccountWithMetadata) { +fn initialize_account(pre_state: AccountWithMetadata) -> (AccountWithMetadata, Account) { let account_to_claim = pre_state.account.clone(); let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values if account_to_claim != Account::default() { - return; + panic!("Invalid input"); } // Continue only if the owner authorized this operation if !is_authorized { - return; + panic!("Invalid input"); } // Noop will result in account being claimed for this program - write_nssa_outputs(vec![pre_state], vec![account_to_claim]); + (pre_state, account_to_claim) } /// Transfers `balance_to_move` native balance from `sender` to `recipient`. -fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) { +fn transfer( + sender: AccountWithMetadata, + recipient: AccountWithMetadata, + balance_to_move: u128, +) -> (Vec, Vec) { // Continue only if the sender has authorized this operation if !sender.is_authorized { - return; + panic!("Invalid input"); } // Continue only if the sender has enough balance if sender.account.balance < balance_to_move { - return; + panic!("Invalid input"); } // Create accounts post states, with updated balances @@ -40,23 +45,30 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance let mut recipient_post = recipient.account.clone(); sender_post.balance -= balance_to_move; recipient_post.balance += balance_to_move; - - write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); + (vec![sender, recipient], vec![sender_post, recipient_post]) } /// A transfer of balance program. /// To be used both in public and private contexts. fn main() { // Read input accounts. - let ProgramInput { - pre_states, - instruction: balance_to_move, - } = read_nssa_inputs(); + let ( + ProgramInput { + pre_states, + instruction: balance_to_move, + }, + instruction_words, + ) = read_nssa_inputs(); match (pre_states.as_slice(), balance_to_move) { - ([account_to_claim], 0) => initialize_account(account_to_claim.clone()), + ([account_to_claim], 0) => { + let (pre, post) = initialize_account(account_to_claim.clone()); + write_nssa_outputs(instruction_words, vec![pre], vec![post]); + } ([sender, recipient], balance_to_move) => { - transfer(sender.clone(), recipient.clone(), balance_to_move) + let (pre_states, post_states) = + transfer(sender.clone(), recipient.clone(), balance_to_move); + write_nssa_outputs(instruction_words, pre_states, post_states); } _ => panic!("invalid params"), } diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index fbea167..d2cd80d 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -1,5 +1,8 @@ use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; -use risc0_zkvm::sha::{Impl, Sha256}; +use risc0_zkvm::{ + serde::to_vec, + sha::{Impl, Sha256}, +}; const PRIZE: u128 = 150; @@ -44,10 +47,13 @@ impl Challenge { fn main() { // Read input accounts. // It is expected to receive only two accounts: [pinata_account, winner_account] - let ProgramInput { - pre_states, - instruction: solution, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: solution, + }, + instruction_data, + ) = read_nssa_inputs::(); let [pinata, winner] = match pre_states.try_into() { Ok(array) => array, @@ -66,5 +72,9 @@ fn main() { pinata_post.data = data.next_data().to_vec(); winner_post.balance += PRIZE; - write_nssa_outputs(vec![pinata, winner], vec![pinata_post, winner_post]); + write_nssa_outputs( + to_vec(&solution).unwrap(), + vec![pinata, winner], + vec![pinata_post, winner_post], + ); } 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 223229f..5d13d46 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -8,7 +8,7 @@ use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, - program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramOutput, validate_execution}, + program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, validate_execution}, }; fn main() { @@ -24,12 +24,30 @@ fn main() { let mut pre_states: Vec = Vec::new(); let mut state_diff: HashMap = HashMap::new(); - let mut program_output = program_outputs[0].clone(); + let num_calls = program_outputs.len(); + if num_calls > MAX_NUMBER_CHAINED_CALLS { + panic!("Max deapth is exceeded"); + } + + if program_outputs[num_calls - 1].chained_call.is_some() { + panic!("Call stack is incomplete"); + } + + for i in 0..(program_outputs.len() - 1) { + let Some(chained_call) = program_outputs[i].chained_call.clone() else { + panic!("Expected chained call"); + }; + + // Check that instruction data in caller is the instruction data in callee + if chained_call.instruction_data != program_outputs[i + 1].instruction_data { + panic!("Invalid instruction data"); + } + } + + for program_output in program_outputs { + let mut program_output = program_output.clone(); - for _i in 0..MAX_NUMBER_CHAINED_CALLS { // Check that `program_output` is consistent with the execution of the corresponding program. - // TODO: Program output should contain the instruction data to verify the chain of call si - // performed correctly. env::verify(program_id, &to_vec(&program_output).unwrap()).unwrap(); // Check that the program is well behaved. @@ -54,7 +72,11 @@ fn main() { .iter() .zip(&program_output.post_states) { - if !state_diff.contains_key(&pre.account_id) { + if let Some(account_pre) = state_diff.get(&pre.account_id) { + if account_pre != &pre.account { + panic!("Invalid input"); + } + } else { pre_states.push(pre.clone()); } state_diff.insert(pre.account_id.clone(), post.clone()); @@ -62,29 +84,6 @@ fn main() { if let Some(next_chained_call) = &program_output.chained_call { program_id = next_chained_call.program_id; - - // // Build post states with metadata for next call - // let mut post_states_with_metadata = Vec::new(); - // for (pre, post) in program_output - // .pre_states - // .iter() - // .zip(program_output.post_states) - // { - // let mut post_with_metadata = pre.clone(); - // post_with_metadata.account = post.clone(); - // post_states_with_metadata.push(post_with_metadata); - // } - - // input_pre_states = next_chained_call - // .account_indices - // .iter() - // .map(|&i| { - // post_states_with_metadata - // .get(i) - // .ok_or_else(|| NssaError::InvalidInput("Invalid account indices".into())) - // .cloned() - // }) - // .collect::, NssaError>>()?; } else { break; }; diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index e5680be..5be4f91 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -170,10 +170,13 @@ fn new_definition( type Instruction = [u8; 23]; fn main() { - let ProgramInput { - pre_states, - instruction, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction, + }, + instruction_words, + ) = read_nssa_inputs::(); match instruction[0] { 0 => { @@ -184,7 +187,7 @@ fn main() { // Execute let post_states = new_definition(&pre_states, name, total_supply); - write_nssa_outputs(pre_states, post_states); + write_nssa_outputs(instruction_words, pre_states, post_states); } 1 => { // Parse instruction @@ -194,7 +197,7 @@ fn main() { // Execute let post_states = transfer(&pre_states, balance_to_move); - write_nssa_outputs(pre_states, post_states); + write_nssa_outputs(instruction_words, pre_states, post_states); } _ => panic!("Invalid instruction"), }; @@ -204,7 +207,7 @@ fn main() { mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; - use crate::{new_definition, transfer, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE}; + use crate::{TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE, new_definition, transfer}; #[should_panic(expected = "Invalid number of input accounts")] #[test] diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 7628e78..96cf583 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -27,6 +27,7 @@ pub fn execute_and_prove( ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { let mut env_builder = ExecutorEnv::builder(); let mut program_outputs = Vec::new(); + for _i in 0..MAX_NUMBER_CHAINED_CALLS { let inner_receipt = execute_and_prove_program(program, pre_states, instruction_data)?; diff --git a/nssa/test_program_methods/guest/src/bin/burner.rs b/nssa/test_program_methods/guest/src/bin/burner.rs index 1ef7373..b5352a4 100644 --- a/nssa/test_program_methods/guest/src/bin/burner.rs +++ b/nssa/test_program_methods/guest/src/bin/burner.rs @@ -1,12 +1,15 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = u128; fn main() { - let ProgramInput { - pre_states, - instruction: balance_to_burn, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: balance_to_burn, + }, + instruction_words, + ) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -17,5 +20,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance -= balance_to_burn; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(instruction_words, vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index dfd77b1..e75f17a 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -8,10 +8,13 @@ type Instruction = (u128, ProgramId); /// A program that calls another program. /// It permutes the order of the input accounts on the subsequent call fn main() { - let ProgramInput { - pre_states, - instruction: (balance, program_id), - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: (balance, program_id), + }, + instruction_words, + ) = read_nssa_inputs::(); let [sender_pre, receiver_pre] = match pre_states.try_into() { Ok(array) => array, @@ -27,6 +30,7 @@ fn main() { }); write_nssa_outputs_with_chained_call( + instruction_words, vec![sender_pre.clone(), receiver_pre.clone()], vec![sender_pre.account, receiver_pre.account], chained_call, diff --git a/nssa/test_program_methods/guest/src/bin/data_changer.rs b/nssa/test_program_methods/guest/src/bin/data_changer.rs index c7d34a2..9cd8992 100644 --- a/nssa/test_program_methods/guest/src/bin/data_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/data_changer.rs @@ -1,9 +1,9 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.data.push(0); - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(instruction_words, vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/extra_output.rs b/nssa/test_program_methods/guest/src/bin/extra_output.rs index 3543d51..64fdea3 100644 --- a/nssa/test_program_methods/guest/src/bin/extra_output.rs +++ b/nssa/test_program_methods/guest/src/bin/extra_output.rs @@ -1,12 +1,12 @@ use nssa_core::{ account::Account, - program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}, + program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -15,5 +15,9 @@ fn main() { let account_pre = pre.account.clone(); - write_nssa_outputs(vec![pre], vec![account_pre, Account::default()]); + write_nssa_outputs( + instruction_words, + vec![pre], + vec![account_pre, Account::default()], + ); } diff --git a/nssa/test_program_methods/guest/src/bin/minter.rs b/nssa/test_program_methods/guest/src/bin/minter.rs index 2ec97a9..08bb488 100644 --- a/nssa/test_program_methods/guest/src/bin/minter.rs +++ b/nssa/test_program_methods/guest/src/bin/minter.rs @@ -1,9 +1,9 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance += 1; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(instruction_words, vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/missing_output.rs b/nssa/test_program_methods/guest/src/bin/missing_output.rs index 7b6016c..8c0a3f6 100644 --- a/nssa/test_program_methods/guest/src/bin/missing_output.rs +++ b/nssa/test_program_methods/guest/src/bin/missing_output.rs @@ -1,9 +1,9 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre1, pre2] = match pre_states.try_into() { Ok(array) => array, @@ -12,5 +12,5 @@ fn main() { let account_pre1 = pre1.account.clone(); - write_nssa_outputs(vec![pre1, pre2], vec![account_pre1]); + write_nssa_outputs(instruction_words, vec![pre1, pre2], vec![account_pre1]); } diff --git a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs index b3b2599..c5d706b 100644 --- a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs @@ -3,7 +3,7 @@ use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. } , instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.nonce += 1; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(instruction_words, vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs index 49947cd..e8afc7c 100644 --- a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs @@ -1,9 +1,9 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7]; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(instruction_words, vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs index 13263c5..e3e64a3 100644 --- a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs +++ b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs @@ -1,12 +1,15 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = u128; fn main() { - let ProgramInput { - pre_states, - instruction: balance, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: balance, + }, + instruction_words, + ) = read_nssa_inputs::(); let [sender_pre, receiver_pre] = match pre_states.try_into() { Ok(array) => array, @@ -19,6 +22,7 @@ fn main() { receiver_post.balance += balance; write_nssa_outputs( + instruction_words, vec![sender_pre, receiver_pre], vec![sender_post, receiver_post], ); From b90837edb6857393cdbcaafe1b389ef3fccf9af6 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 20 Nov 2025 01:40:05 -0300 Subject: [PATCH 07/70] add test wip --- .../src/bin/privacy_preserving_circuit.rs | 8 +-- .../privacy_preserving_transaction/circuit.rs | 38 +++++++++++- nssa/src/state.rs | 62 ++++++++++++++++++- 3 files changed, 100 insertions(+), 8 deletions(-) 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 5d13d46..888cb5b 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -26,7 +26,7 @@ fn main() { let num_calls = program_outputs.len(); if num_calls > MAX_NUMBER_CHAINED_CALLS { - panic!("Max deapth is exceeded"); + panic!("Max depth is exceeded"); } if program_outputs[num_calls - 1].chained_call.is_some() { @@ -44,7 +44,7 @@ fn main() { } } - for program_output in program_outputs { + for (i, program_output) in program_outputs.iter().enumerate() { let mut program_output = program_output.clone(); // Check that `program_output` is consistent with the execution of the corresponding program. @@ -84,8 +84,8 @@ fn main() { if let Some(next_chained_call) = &program_output.chained_call { program_id = next_chained_call.program_id; - } else { - break; + } else if i != program_outputs.len() - 1 { + panic!("Inner call without a chained call found") }; } diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 96cf583..e0df466 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; + use nssa_core::{ MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, account::AccountWithMetadata, - program::{InstructionData, ProgramOutput}, + program::{InstructionData, ProgramId, ProgramOutput}, }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; @@ -24,12 +26,16 @@ pub fn execute_and_prove( private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], private_account_auth: &[(NullifierSecretKey, MembershipProof)], program: &Program, + programs: &HashMap, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { + let mut program = program; + let mut instruction_data = instruction_data.clone(); + let mut pre_states = pre_states.to_vec(); let mut env_builder = ExecutorEnv::builder(); let mut program_outputs = Vec::new(); for _i in 0..MAX_NUMBER_CHAINED_CALLS { - let inner_receipt = execute_and_prove_program(program, pre_states, instruction_data)?; + let inner_receipt = execute_and_prove_program(program, &pre_states, &instruction_data)?; let program_output: ProgramOutput = inner_receipt .journal @@ -42,7 +48,33 @@ pub fn execute_and_prove( // Prove circuit. env_builder.add_assumption(inner_receipt); - if program_output.chained_call.is_none() { + if let Some(next_call) = program_output.chained_call { + // TODO: remove unwrap + program = programs.get(&next_call.program_id).unwrap(); + instruction_data = next_call.instruction_data.clone(); + // Build post states with metadata for next call + let mut post_states_with_metadata = Vec::new(); + for (pre, post) in program_output + .pre_states + .iter() + .zip(program_output.post_states) + { + let mut post_with_metadata = pre.clone(); + post_with_metadata.account = post.clone(); + post_states_with_metadata.push(post_with_metadata); + } + + pre_states = next_call + .account_indices + .iter() + .map(|&i| { + post_states_with_metadata + .get(i) + .ok_or_else(|| NssaError::InvalidInput("Invalid account indices".into())) + .cloned() + }) + .collect::, NssaError>>()?; + } else { break; } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index be3d0ab..0521aa7 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2081,7 +2081,7 @@ pub mod tests { } #[test] - fn test_chained_call() { + fn test_public_chained_call() { let program = Program::chain_caller(); let key = PrivateKey::try_new([1; 32]).unwrap(); let address = Address::from(&PublicKey::new_from_private_key(&key)); @@ -2119,4 +2119,64 @@ pub mod tests { assert_eq!(from_post.balance, initial_balance - amount); assert_eq!(to_post, expected_to_post); } + + #[test] + fn test_private_chained_call() { + let program = Program::chain_caller(); + let from_keys = test_private_account_keys_1(); + let to_keys = test_private_account_keys_1(); + let initial_balance = 100; + let from_account = AccountWithMetadata::new( + Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: initial_balance, + ..Account::default() + }, + true, + &from_keys.npk(), + ); + let to_account = AccountWithMetadata::new(Account::default(), true, &from_keys.npk()); + let from_commitment = Commitment::new(&from_keys.npk(), &from_account.account); + let mut state = V02State::new_with_genesis_accounts(&[], &[from_commitment.clone()]) + .with_test_programs(); + // let from = address; + // let from_key = key; + // let to = Address::new([2; 32]); + let amount: u128 = 37; + let instruction: (u128, ProgramId) = + (amount, Program::authenticated_transfer_program().id()); + + let from_esk = [3; 32]; + let from_ss = SharedSecretKey::new(&from_esk, &from_keys.ivk()); + let from_epk = EphemeralPublicKey::from_scalar(from_esk); + + let to_esk = [4; 32]; + let to_ss = SharedSecretKey::new(&to_esk, &to_keys.ivk()); + let to_epk = EphemeralPublicKey::from_scalar(to_esk); + + let (output, proof) = execute_and_prove( + &[from_account, to_account], + &Program::serialize_instruction(instruction).unwrap(), + &[1, 2], + &[0xdeadbeef1, 0xdeadbeef2], + &[(from_keys.npk(), from_ss), (to_keys.npk(), to_ss)], + &[( + from_keys.nsk, + state.get_proof_for_commitment(&from_commitment).unwrap(), + )], + &program, + ) + .unwrap(); + + let message = Message::try_from_circuit_output(vec![], vec![], vec![], output).unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + // + // state.transition_from_public_transaction(&tx).unwrap(); + // + // let from_post = state.get_account_by_address(&from); + // let to_post = state.get_account_by_address(&to); + // assert_eq!(from_post.balance, initial_balance - amount); + // assert_eq!(to_post, expected_to_post); + } } From b59cd0da9269c295c5beb78f27bc6ad9927f72e0 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 20 Nov 2025 19:25:56 -0300 Subject: [PATCH 08/70] wip --- integration_tests/src/tps_test_utils.rs | 2 +- .../privacy_preserving_transaction/circuit.rs | 25 +-- nssa/src/program.rs | 23 +++ nssa/src/state.rs | 150 +++++++++++------- wallet/src/pinata_interactions.rs | 4 +- wallet/src/transaction_utils.rs | 16 +- 6 files changed, 140 insertions(+), 80 deletions(-) diff --git a/integration_tests/src/tps_test_utils.rs b/integration_tests/src/tps_test_utils.rs index 1e31c02..23ce944 100644 --- a/integration_tests/src/tps_test_utils.rs +++ b/integration_tests/src/tps_test_utils.rs @@ -169,7 +169,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { (recipient_npk.clone(), recipient_ss), ], &[(sender_nsk, proof)], - &program, + &program.into(), ) .unwrap(); let message = pptx::message::Message::try_from_circuit_output( diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index e0df466..2b5c655 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -8,7 +8,11 @@ use nssa_core::{ }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; -use crate::{error::NssaError, program::Program, state::MAX_NUMBER_CHAINED_CALLS}; +use crate::{ + error::NssaError, + program::{Program, ProgramWithDependencies}, + state::MAX_NUMBER_CHAINED_CALLS, +}; use crate::program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}; @@ -25,17 +29,17 @@ pub fn execute_and_prove( private_account_nonces: &[u128], private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], private_account_auth: &[(NullifierSecretKey, MembershipProof)], - program: &Program, - programs: &HashMap, + program_with_dependencies: &ProgramWithDependencies, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { - let mut program = program; + let mut program = &program_with_dependencies.program; + let dependencies = &program_with_dependencies.dependencies; let mut instruction_data = instruction_data.clone(); let mut pre_states = pre_states.to_vec(); let mut env_builder = ExecutorEnv::builder(); let mut program_outputs = Vec::new(); for _i in 0..MAX_NUMBER_CHAINED_CALLS { - let inner_receipt = execute_and_prove_program(program, &pre_states, &instruction_data)?; + let inner_receipt = execute_and_prove_program(&program, &pre_states, &instruction_data)?; let program_output: ProgramOutput = inner_receipt .journal @@ -49,8 +53,9 @@ pub fn execute_and_prove( env_builder.add_assumption(inner_receipt); if let Some(next_call) = program_output.chained_call { - // TODO: remove unwrap - program = programs.get(&next_call.program_id).unwrap(); + program = dependencies + .get(&next_call.program_id) + .ok_or(NssaError::InvalidProgramBehavior)?; instruction_data = next_call.instruction_data.clone(); // Build post states with metadata for next call let mut post_states_with_metadata = Vec::new(); @@ -85,7 +90,7 @@ pub fn execute_and_prove( private_account_nonces: private_account_nonces.to_vec(), private_account_keys: private_account_keys.to_vec(), private_account_auth: private_account_auth.to_vec(), - program_id: program.id(), + program_id: program_with_dependencies.program.id(), }; env_builder.write(&circuit_input).unwrap(); @@ -198,7 +203,7 @@ mod tests { &[0xdeadbeef], &[(recipient_keys.npk(), shared_secret.clone())], &[], - &Program::authenticated_transfer_program(), + &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -299,7 +304,7 @@ mod tests { sender_keys.nsk, commitment_set.get_proof_for(&commitment_sender).unwrap(), )], - &program, + &program.into(), ) .unwrap(); diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 11eb413..bca6fb9 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}; use nssa_core::{ account::AccountWithMetadata, @@ -96,6 +98,27 @@ impl Program { } } +pub struct ProgramWithDependencies { + pub program: Program, + // TODO: this will have a copy of each dependency bytecode in each program + pub dependencies: HashMap, +} + +impl ProgramWithDependencies { + pub fn new(program: Program, dependencies: HashMap) -> Self { + Self { + program, + dependencies, + } + } +} + +impl From for ProgramWithDependencies { + fn from(program: Program) -> Self { + ProgramWithDependencies::new(program, HashMap::new()) + } +} + // TODO: Testnet only. Refactor to prevent compilation on mainnet. impl Program { pub fn pinata() -> Self { diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 0521aa7..726ff77 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -250,7 +250,7 @@ pub mod tests { privacy_preserving_transaction::{ PrivacyPreservingTransaction, circuit, message::Message, witness_set::WitnessSet, }, - program::Program, + program::{Program, ProgramWithDependencies}, public_transaction, signature::PrivateKey, }; @@ -838,7 +838,7 @@ pub mod tests { &[0xdeadbeef], &[(recipient_keys.npk(), shared_secret)], &[], - &Program::authenticated_transfer_program(), + &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -890,7 +890,7 @@ pub mod tests { sender_keys.nsk, state.get_proof_for_commitment(&sender_commitment).unwrap(), )], - &program, + &program.into(), ) .unwrap(); @@ -942,7 +942,7 @@ pub mod tests { sender_keys.nsk, state.get_proof_for_commitment(&sender_commitment).unwrap(), )], - &program, + &program.into(), ) .unwrap(); @@ -1154,7 +1154,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1180,7 +1180,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1206,7 +1206,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1232,7 +1232,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1258,7 +1258,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1293,7 +1293,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1319,7 +1319,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1354,7 +1354,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1391,7 +1391,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1432,7 +1432,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1466,7 +1466,7 @@ pub mod tests { &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1507,7 +1507,7 @@ pub mod tests { ), ], &private_account_auth, - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1555,7 +1555,7 @@ pub mod tests { &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, &private_account_auth, - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1601,7 +1601,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1648,7 +1648,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1694,7 +1694,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1740,7 +1740,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1784,7 +1784,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1813,7 +1813,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1855,7 +1855,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1901,7 +1901,7 @@ pub mod tests { &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1947,7 +1947,7 @@ pub mod tests { ), ], &private_account_auth, - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -2038,7 +2038,7 @@ pub mod tests { (sender_keys.npk(), shared_secret), ], &private_account_auth, - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -2122,61 +2122,93 @@ pub mod tests { #[test] fn test_private_chained_call() { - let program = Program::chain_caller(); + let chain_caller = Program::chain_caller(); + let auth_transfers = Program::authenticated_transfer_program(); let from_keys = test_private_account_keys_1(); - let to_keys = test_private_account_keys_1(); + let to_keys = test_private_account_keys_2(); let initial_balance = 100; let from_account = AccountWithMetadata::new( Account { - program_owner: Program::authenticated_transfer_program().id(), + program_owner: auth_transfers.id(), balance: initial_balance, ..Account::default() }, true, &from_keys.npk(), ); - let to_account = AccountWithMetadata::new(Account::default(), true, &from_keys.npk()); + let to_account = AccountWithMetadata::new( + Account { + program_owner: auth_transfers.id(), + ..Account::default() + }, + true, + &to_keys.npk(), + ); let from_commitment = Commitment::new(&from_keys.npk(), &from_account.account); - let mut state = V02State::new_with_genesis_accounts(&[], &[from_commitment.clone()]) - .with_test_programs(); - // let from = address; - // let from_key = key; - // let to = Address::new([2; 32]); + let to_commitment = Commitment::new(&to_keys.npk(), &to_account.account); + let state = V02State::new_with_genesis_accounts( + &[], + &[from_commitment.clone(), to_commitment.clone()], + ) + .with_test_programs(); let amount: u128 = 37; let instruction: (u128, ProgramId) = (amount, Program::authenticated_transfer_program().id()); let from_esk = [3; 32]; let from_ss = SharedSecretKey::new(&from_esk, &from_keys.ivk()); - let from_epk = EphemeralPublicKey::from_scalar(from_esk); + // let from_epk = EphemeralPublicKey::from_scalar(from_esk); - let to_esk = [4; 32]; + let to_esk = [3; 32]; let to_ss = SharedSecretKey::new(&to_esk, &to_keys.ivk()); - let to_epk = EphemeralPublicKey::from_scalar(to_esk); + // let to_epk = EphemeralPublicKey::from_scalar(to_esk); + // + let mut dependencies = HashMap::new(); - let (output, proof) = execute_and_prove( - &[from_account, to_account], + dependencies.insert(auth_transfers.id(), auth_transfers); + let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies); + + let result = execute_and_prove( + &[to_account, from_account], &Program::serialize_instruction(instruction).unwrap(), - &[1, 2], + &[1, 1], &[0xdeadbeef1, 0xdeadbeef2], - &[(from_keys.npk(), from_ss), (to_keys.npk(), to_ss)], - &[( - from_keys.nsk, - state.get_proof_for_commitment(&from_commitment).unwrap(), - )], - &program, + &[(to_keys.npk(), from_ss), (from_keys.npk(), to_ss)], + &[ + ( + to_keys.nsk, + state.get_proof_for_commitment(&to_commitment).unwrap(), + ), + ( + from_keys.nsk, + state.get_proof_for_commitment(&from_commitment).unwrap(), + ), + ], + &program_with_deps, ) .unwrap(); - - let message = Message::try_from_circuit_output(vec![], vec![], vec![], output).unwrap(); - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - // - // state.transition_from_public_transaction(&tx).unwrap(); - // - // let from_post = state.get_account_by_address(&from); - // let to_post = state.get_account_by_address(&to); - // assert_eq!(from_post.balance, initial_balance - amount); - // assert_eq!(to_post, expected_to_post); } + + // let expected_to_post = Account { + // program_owner: Program::chain_caller().id(), + // balance: amount, + // ..Account::default() + // }; + // + // let message = public_transaction::Message::try_new( + // program.id(), + // vec![to, from], //The chain_caller program permutes the account order in the chain call + // vec![0], + // instruction, + // ) + // .unwrap(); + // let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + // let tx = PublicTransaction::new(message, witness_set); + // + // state.transition_from_public_transaction(&tx).unwrap(); + // + // let from_post = state.get_account_by_address(&from); + // let to_post = state.get_account_by_address(&to); + // assert_eq!(from_post.balance, initial_balance - amount); + // assert_eq!(to_post, expected_to_post); } diff --git a/wallet/src/pinata_interactions.rs b/wallet/src/pinata_interactions.rs index 6e3e5ce..0f20475 100644 --- a/wallet/src/pinata_interactions.rs +++ b/wallet/src/pinata_interactions.rs @@ -59,7 +59,7 @@ impl WalletCore { &produce_random_nonces(1), &[(winner_npk.clone(), shared_secret_winner.clone())], &[(winner_nsk.unwrap(), winner_proof)], - &program, + &program.into(), ) .unwrap(); @@ -125,7 +125,7 @@ impl WalletCore { &produce_random_nonces(1), &[(winner_npk.clone(), shared_secret_winner.clone())], &[], - &program, + &program.into(), ) .unwrap(); diff --git a/wallet/src/transaction_utils.rs b/wallet/src/transaction_utils.rs index 2dd69ca..d99a480 100644 --- a/wallet/src/transaction_utils.rs +++ b/wallet/src/transaction_utils.rs @@ -110,7 +110,7 @@ impl WalletCore { (from_nsk.unwrap(), from_proof.unwrap()), (to_nsk.unwrap(), to_proof), ], - &program, + &program.into(), ) .unwrap(); @@ -184,7 +184,7 @@ impl WalletCore { (to_npk.clone(), shared_secret_to.clone()), ], &[(from_nsk.unwrap(), from_proof.unwrap())], - &program, + &program.into(), ) .unwrap(); @@ -254,7 +254,7 @@ impl WalletCore { (to_npk.clone(), shared_secret_to.clone()), ], &[(from_nsk.unwrap(), from_proof.unwrap())], - &program, + &program.into(), ) .unwrap(); @@ -321,7 +321,7 @@ impl WalletCore { &produce_random_nonces(1), &[(from_npk.clone(), shared_secret.clone())], &[(from_nsk.unwrap(), from_proof.unwrap())], - &program, + &program.into(), ) .unwrap(); @@ -382,7 +382,7 @@ impl WalletCore { &produce_random_nonces(1), &[(to_npk.clone(), shared_secret.clone())], &[(to_nsk.unwrap(), to_proof)], - &program, + &program.into(), ) .unwrap(); @@ -448,7 +448,7 @@ impl WalletCore { &produce_random_nonces(1), &[(to_npk.clone(), shared_secret.clone())], &[], - &program, + &program.into(), ) .unwrap(); @@ -510,7 +510,7 @@ impl WalletCore { &produce_random_nonces(1), &[(to_npk.clone(), shared_secret.clone())], &[], - &program, + &program.into(), ) .unwrap(); @@ -562,7 +562,7 @@ impl WalletCore { &produce_random_nonces(1), &[(from_npk.clone(), shared_secret_from.clone())], &[], - &Program::authenticated_transfer_program(), + &Program::authenticated_transfer_program().into(), ) .unwrap(); From bfe38b012ef691eeaf5339d30570bb0b020ecf98 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 22 Nov 2025 16:39:34 -0300 Subject: [PATCH 09/70] small refactor --- .../src/bin/privacy_preserving_circuit.rs | 2 +- .../privacy_preserving_transaction/circuit.rs | 27 +++++++++++++++---- nssa/src/program.rs | 21 --------------- nssa/src/state.rs | 12 +++------ 4 files changed, 26 insertions(+), 36 deletions(-) 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 888cb5b..a4ed28b 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -34,7 +34,7 @@ fn main() { } for i in 0..(program_outputs.len() - 1) { - let Some(chained_call) = program_outputs[i].chained_call.clone() else { + let Some(chained_call) = &program_outputs[i].chained_call else { panic!("Expected chained call"); }; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 2b5c655..bd786b1 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -8,11 +8,7 @@ use nssa_core::{ }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; -use crate::{ - error::NssaError, - program::{Program, ProgramWithDependencies}, - state::MAX_NUMBER_CHAINED_CALLS, -}; +use crate::{error::NssaError, program::Program, state::MAX_NUMBER_CHAINED_CALLS}; use crate::program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}; @@ -20,6 +16,27 @@ use crate::program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_ #[derive(Debug, Clone, PartialEq, Eq)] pub struct Proof(pub(crate) Vec); +pub struct ProgramWithDependencies { + pub program: Program, + // TODO: this will have a copy of each dependency bytecode in each program + pub dependencies: HashMap, +} + +impl ProgramWithDependencies { + pub fn new(program: Program, dependencies: HashMap) -> Self { + Self { + program, + dependencies, + } + } +} + +impl From for ProgramWithDependencies { + fn from(program: Program) -> Self { + ProgramWithDependencies::new(program, HashMap::new()) + } +} + /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution /// circuit pub fn execute_and_prove( diff --git a/nssa/src/program.rs b/nssa/src/program.rs index bca6fb9..91fada4 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -98,27 +98,6 @@ impl Program { } } -pub struct ProgramWithDependencies { - pub program: Program, - // TODO: this will have a copy of each dependency bytecode in each program - pub dependencies: HashMap, -} - -impl ProgramWithDependencies { - pub fn new(program: Program, dependencies: HashMap) -> Self { - Self { - program, - dependencies, - } - } -} - -impl From for ProgramWithDependencies { - fn from(program: Program) -> Self { - ProgramWithDependencies::new(program, HashMap::new()) - } -} - // TODO: Testnet only. Refactor to prevent compilation on mainnet. impl Program { pub fn pinata() -> Self { diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 726ff77..0f83a15 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -244,15 +244,9 @@ pub mod tests { use std::collections::HashMap; use crate::{ - Address, PublicKey, PublicTransaction, V02State, - error::NssaError, - execute_and_prove, - privacy_preserving_transaction::{ - PrivacyPreservingTransaction, circuit, message::Message, witness_set::WitnessSet, - }, - program::{Program, ProgramWithDependencies}, - public_transaction, - signature::PrivateKey, + error::NssaError, execute_and_prove, privacy_preserving_transaction::{ + circuit::{self, ProgramWithDependencies}, message::Message, witness_set::WitnessSet, PrivacyPreservingTransaction + }, program::Program, public_transaction, signature::PrivateKey, Address, PublicKey, PublicTransaction, V02State }; use nssa_core::{ From 386d958c884db452c21a626a012b21fde8aa794c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 22 Nov 2025 16:39:56 -0300 Subject: [PATCH 10/70] fmt, clippy --- nssa/core/src/program.rs | 2 +- .../src/privacy_preserving_transaction/circuit.rs | 2 +- nssa/src/program.rs | 1 - nssa/src/state.rs | 15 ++++++++++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 7f6038f..54a7a97 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -142,7 +142,7 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo let number_of_accounts = pre_states.len(); let number_of_account_ids = pre_states .iter() - .map(|account| account.account_id.clone()) + .map(|account| account.account_id) .collect::>() .len(); diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index bd786b1..37f07dd 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -56,7 +56,7 @@ pub fn execute_and_prove( let mut program_outputs = Vec::new(); for _i in 0..MAX_NUMBER_CHAINED_CALLS { - let inner_receipt = execute_and_prove_program(&program, &pre_states, &instruction_data)?; + let inner_receipt = execute_and_prove_program(program, &pre_states, &instruction_data)?; let program_output: ProgramOutput = inner_receipt .journal diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 91fada4..f646798 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use crate::program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}; use nssa_core::{ diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 0f83a15..8c07116 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -244,9 +244,18 @@ pub mod tests { use std::collections::HashMap; use crate::{ - error::NssaError, execute_and_prove, privacy_preserving_transaction::{ - circuit::{self, ProgramWithDependencies}, message::Message, witness_set::WitnessSet, PrivacyPreservingTransaction - }, program::Program, public_transaction, signature::PrivateKey, Address, PublicKey, PublicTransaction, V02State + Address, PublicKey, PublicTransaction, V02State, + error::NssaError, + execute_and_prove, + privacy_preserving_transaction::{ + PrivacyPreservingTransaction, + circuit::{self, ProgramWithDependencies}, + message::Message, + witness_set::WitnessSet, + }, + program::Program, + public_transaction, + signature::PrivateKey, }; use nssa_core::{ From 47a7656ec4724d6739ef6ab1299b7bce6234569a Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 22 Nov 2025 17:48:29 -0300 Subject: [PATCH 11/70] add asserts in test --- nssa/core/src/program.rs | 2 +- .../guest/src/bin/authenticated_transfer.rs | 1 - nssa/program_methods/guest/src/bin/pinata.rs | 2 +- .../src/bin/privacy_preserving_circuit.rs | 2 +- nssa/src/program.rs | 1 - nssa/src/state.rs | 82 ++++++++++++------- 6 files changed, 57 insertions(+), 33 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 54a7a97..dfa1c0f 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -142,7 +142,7 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo let number_of_accounts = pre_states.len(); let number_of_account_ids = pre_states .iter() - .map(|account| account.account_id) + .map(|account| &account.account_id) .collect::>() .len(); diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index 2e05492..c2e1037 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -2,7 +2,6 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; -use risc0_zkvm::serde::to_vec; /// Initializes a default account under the ownership of this program. /// This is achieved by a noop. diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index d2cd80d..92a6f46 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -52,7 +52,7 @@ fn main() { pre_states, instruction: solution, }, - instruction_data, + _, ) = read_nssa_inputs::(); let [pinata, winner] = match pre_states.try_into() { 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 a4ed28b..97a49a0 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use risc0_zkvm::{guest::env, serde::to_vec}; diff --git a/nssa/src/program.rs b/nssa/src/program.rs index f646798..11eb413 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,4 +1,3 @@ - use crate::program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}; use nssa_core::{ account::AccountWithMetadata, diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 8c07116..efa17d3 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2125,6 +2125,7 @@ pub mod tests { #[test] fn test_private_chained_call() { + // Arrange let chain_caller = Program::chain_caller(); let auth_transfers = Program::authenticated_transfer_program(); let from_keys = test_private_account_keys_1(); @@ -2147,9 +2148,10 @@ pub mod tests { true, &to_keys.npk(), ); + let from_commitment = Commitment::new(&from_keys.npk(), &from_account.account); let to_commitment = Commitment::new(&to_keys.npk(), &to_account.account); - let state = V02State::new_with_genesis_accounts( + let mut state = V02State::new_with_genesis_accounts( &[], &[from_commitment.clone(), to_commitment.clone()], ) @@ -2160,22 +2162,40 @@ pub mod tests { let from_esk = [3; 32]; let from_ss = SharedSecretKey::new(&from_esk, &from_keys.ivk()); - // let from_epk = EphemeralPublicKey::from_scalar(from_esk); + let from_epk = EphemeralPublicKey::from_scalar(from_esk); let to_esk = [3; 32]; let to_ss = SharedSecretKey::new(&to_esk, &to_keys.ivk()); - // let to_epk = EphemeralPublicKey::from_scalar(to_esk); + let to_epk = EphemeralPublicKey::from_scalar(to_esk); // let mut dependencies = HashMap::new(); dependencies.insert(auth_transfers.id(), auth_transfers); let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies); - let result = execute_and_prove( + let from_new_nonce = 0xdeadbeef1; + let to_new_nonce = 0xdeadbeef1; + + let from_expected_post = Account { + balance: initial_balance - amount, + nonce: from_new_nonce, + ..from_account.account.clone() + }; + let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post); + + let to_expected_post = Account { + balance: amount, + nonce: to_new_nonce, + ..to_account.account.clone() + }; + let to_expected_commitment = Commitment::new(&to_keys.npk(), &to_expected_post); + + // Act + let (output, proof) = execute_and_prove( &[to_account, from_account], &Program::serialize_instruction(instruction).unwrap(), &[1, 1], - &[0xdeadbeef1, 0xdeadbeef2], + &[to_new_nonce, from_new_nonce], &[(to_keys.npk(), from_ss), (from_keys.npk(), to_ss)], &[ ( @@ -2190,28 +2210,34 @@ pub mod tests { &program_with_deps, ) .unwrap(); - } - // let expected_to_post = Account { - // program_owner: Program::chain_caller().id(), - // balance: amount, - // ..Account::default() - // }; - // - // let message = public_transaction::Message::try_new( - // program.id(), - // vec![to, from], //The chain_caller program permutes the account order in the chain call - // vec![0], - // instruction, - // ) - // .unwrap(); - // let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); - // let tx = PublicTransaction::new(message, witness_set); - // - // state.transition_from_public_transaction(&tx).unwrap(); - // - // let from_post = state.get_account_by_address(&from); - // let to_post = state.get_account_by_address(&to); - // assert_eq!(from_post.balance, initial_balance - amount); - // assert_eq!(to_post, expected_to_post); + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![ + (to_keys.npk(), to_keys.ivk(), to_epk), + (from_keys.npk(), from_keys.ivk(), from_epk), + ], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let transaction = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&transaction) + .unwrap(); + + // Assert + assert!( + state + .get_proof_for_commitment(&from_expected_commitment) + .is_some() + ); + assert!( + state + .get_proof_for_commitment(&to_expected_commitment) + .is_some() + ); + } } From d73fcbd2b31add0017c3a42187186f98d16667ee Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 22 Nov 2025 18:30:15 -0300 Subject: [PATCH 12/70] add docstrings --- nssa/core/src/program.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index dfa1c0f..a26a024 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -18,7 +18,9 @@ pub struct ProgramInput { #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ChainedCall { + /// The program ID of the program to execute pub program_id: ProgramId, + /// The instruction data to pass pub instruction_data: InstructionData, pub account_indices: Vec, } @@ -26,9 +28,13 @@ pub struct ChainedCall { #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ProgramOutput { + /// The instruction data the program received to produce this output pub instruction_data: InstructionData, + /// The account pre states the program received to produce this output pub pre_states: Vec, + /// The account post states produced with the given pre states and instruction data pub post_states: Vec, + /// The optional next call of a program pub chained_call: Option, } From 0054ce58471746ae8629106a69934f3d8bf4683b Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 22 Nov 2025 18:39:02 -0300 Subject: [PATCH 13/70] nit --- nssa/src/privacy_preserving_transaction/circuit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 37f07dd..4640d58 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -18,7 +18,7 @@ pub struct Proof(pub(crate) Vec); pub struct ProgramWithDependencies { pub program: Program, - // TODO: this will have a copy of each dependency bytecode in each program + // TODO: avoid having a copy of the bytecode of each dependency. pub dependencies: HashMap, } From 8c9b46b0df441c5e09867d7a3cd25f9f46fd76f7 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 22 Nov 2025 20:15:04 -0300 Subject: [PATCH 14/70] hot fix for integration test --- integration_tests/src/data_changer.bin | Bin 371388 -> 371808 bytes integration_tests/src/test_suite_map.rs | 5 ++--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/integration_tests/src/data_changer.bin b/integration_tests/src/data_changer.bin index c4fbec0f1c44d11a802a9f10e241da57d768ea88..45df6fd2fc46283c92933619c04a8a048bd60bfc 100644 GIT binary patch delta 95738 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 R9iL9}`abjO;jLzl{eSpicRT<9 diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 0bbc8b0..8605a42 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1180,9 +1180,8 @@ pub fn prepare_function_map() -> HashMap { let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; - println!("Waiting for next blocks to check if continoius run fetch account"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + println!("Waiting for next blocks to check if continuous run fetch account"); + tokio::time::sleep(Duration::from_secs(3 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) .await From 425f0ccc1b1564d024fc8bb79347547f7a50770c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy <41742639+schouhy@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:20:56 -0300 Subject: [PATCH 15/70] Update nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs Co-authored-by: Daniil Polyakov --- .../program_methods/guest/src/bin/privacy_preserving_circuit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 97a49a0..a3475a1 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -26,7 +26,7 @@ fn main() { let num_calls = program_outputs.len(); if num_calls > MAX_NUMBER_CHAINED_CALLS { - panic!("Max depth is exceeded"); + panic!("Max chained calls depth is exceeded"); } if program_outputs[num_calls - 1].chained_call.is_some() { From 8e1c53bd4ef03e6313c813065cb964f848937e1f Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 25 Nov 2025 14:51:02 -0300 Subject: [PATCH 16/70] use instruction_words from read in pinata --- nssa/program_methods/guest/src/bin/pinata.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index 92a6f46..a92fb33 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -52,7 +52,7 @@ fn main() { pre_states, instruction: solution, }, - _, + instruction_words, ) = read_nssa_inputs::(); let [pinata, winner] = match pre_states.try_into() { @@ -73,7 +73,7 @@ fn main() { winner_post.balance += PRIZE; write_nssa_outputs( - to_vec(&solution).unwrap(), + instruction_words, vec![pinata, winner], vec![pinata_post, winner_post], ); From 3f636465f65f1aa40a02242588289cde4dd170d6 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 25 Nov 2025 14:51:28 -0300 Subject: [PATCH 17/70] avoid using manual indexing in vector --- .../program_methods/guest/src/bin/privacy_preserving_circuit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 97a49a0..7bca005 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -29,7 +29,7 @@ fn main() { panic!("Max depth is exceeded"); } - if program_outputs[num_calls - 1].chained_call.is_some() { + if program_outputs.last().and_then(|last| last.chained_call.as_ref()).is_some() { panic!("Call stack is incomplete"); } From ba556bee0ba4db5e1cbbc488f9f03c0fc9e83a47 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 25 Nov 2025 15:03:17 -0300 Subject: [PATCH 18/70] nit --- nssa/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nssa/src/state.rs b/nssa/src/state.rs index efa17d3..2592022 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2167,7 +2167,7 @@ pub mod tests { let to_esk = [3; 32]; let to_ss = SharedSecretKey::new(&to_esk, &to_keys.ivk()); let to_epk = EphemeralPublicKey::from_scalar(to_esk); - // + let mut dependencies = HashMap::new(); dependencies.insert(auth_transfers.id(), auth_transfers); From a3061afebe2a8ef8ceb4c4acbb15d04bfb2a8df7 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 25 Nov 2025 15:15:14 -0300 Subject: [PATCH 19/70] replace unwrap with expect --- .../guest/src/bin/privacy_preserving_circuit.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 540e56b..67a916f 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -29,7 +29,11 @@ fn main() { panic!("Max chained calls depth is exceeded"); } - if program_outputs.last().and_then(|last| last.chained_call.as_ref()).is_some() { + if program_outputs + .last() + .and_then(|last| last.chained_call.as_ref()) + .is_some() + { panic!("Call stack is incomplete"); } @@ -48,7 +52,10 @@ fn main() { let mut program_output = program_output.clone(); // Check that `program_output` is consistent with the execution of the corresponding program. - env::verify(program_id, &to_vec(&program_output).unwrap()).unwrap(); + let program_output_words = + &to_vec(&program_output).expect("program_output must be serializable"); + env::verify(program_id, program_output_words) + .expect("program output must match the program's execution"); // Check that the program is well behaved. // See the # Programs section for the definition of the `validate_execution` method. From bc24f006ef389d3f6e6cf543f902adf2a8611416 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 26 Nov 2025 17:37:22 -0300 Subject: [PATCH 20/70] use window --- nssa/program_methods/guest/src/bin/pinata.rs | 1 - .../guest/src/bin/privacy_preserving_circuit.rs | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index a92fb33..377d6a6 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -1,6 +1,5 @@ use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; use risc0_zkvm::{ - serde::to_vec, sha::{Impl, Sha256}, }; 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 67a916f..18556f9 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -37,13 +37,16 @@ fn main() { panic!("Call stack is incomplete"); } - for i in 0..(program_outputs.len() - 1) { - let Some(chained_call) = &program_outputs[i].chained_call else { + for window in program_outputs.windows(2) { + let caller = &window[0]; + let callee = &window[1]; + + let Some(chained_call) = &caller.chained_call else { panic!("Expected chained call"); }; // Check that instruction data in caller is the instruction data in callee - if chained_call.instruction_data != program_outputs[i + 1].instruction_data { + if chained_call.instruction_data != callee.instruction_data { panic!("Invalid instruction data"); } } From 1df24eb11f0c3eacf9a8ed796b4af401619a35df Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 26 Nov 2025 17:41:49 -0300 Subject: [PATCH 21/70] call write_nssa_outputs function once --- .../guest/src/bin/authenticated_transfer.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index c2e1037..c179d57 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -59,16 +59,16 @@ fn main() { instruction_words, ) = read_nssa_inputs(); - match (pre_states.as_slice(), balance_to_move) { + let (pre_states, post_states) = match (pre_states.as_slice(), balance_to_move) { ([account_to_claim], 0) => { let (pre, post) = initialize_account(account_to_claim.clone()); - write_nssa_outputs(instruction_words, vec![pre], vec![post]); + (vec![pre], vec![post]) } ([sender, recipient], balance_to_move) => { - let (pre_states, post_states) = - transfer(sender.clone(), recipient.clone(), balance_to_move); - write_nssa_outputs(instruction_words, pre_states, post_states); + transfer(sender.clone(), recipient.clone(), balance_to_move) } _ => panic!("invalid params"), - } + }; + + write_nssa_outputs(instruction_words, pre_states, post_states); } From c94d353b54009acf55be5182c9af3d12db61a138 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 27 Nov 2025 22:07:53 +0300 Subject: [PATCH 22/70] refactor: move some stuff to a more suitable place --- integration_tests/src/test_suite_map.rs | 120 ++++----- .../mod.rs => chain_storage.rs} | 0 wallet/src/cli/account.rs | 9 +- wallet/src/cli/chain.rs | 5 +- wallet/src/cli/config.rs | 5 +- wallet/src/cli/mod.rs | 172 ++++++++++++- wallet/src/cli/programs/mod.rs | 3 + .../native_token_transfer.rs} | 4 +- .../{pinata_program.rs => programs/pinata.rs} | 4 +- .../{token_program.rs => programs/token.rs} | 4 +- wallet/src/helperfunctions.rs | 83 +++++- wallet/src/lib.rs | 239 +----------------- wallet/src/main.rs | 24 +- wallet/src/program_interactions/mod.rs | 2 + .../pinata.rs} | 0 .../token.rs} | 0 16 files changed, 348 insertions(+), 326 deletions(-) rename wallet/src/{chain_storage/mod.rs => chain_storage.rs} (100%) create mode 100644 wallet/src/cli/programs/mod.rs rename wallet/src/cli/{native_token_transfer_program.rs => programs/native_token_transfer.rs} (99%) rename wallet/src/cli/{pinata_program.rs => programs/pinata.rs} (99%) rename wallet/src/cli/{token_program.rs => programs/token.rs} (99%) create mode 100644 wallet/src/program_interactions/mod.rs rename wallet/src/{pinata_interactions.rs => program_interactions/pinata.rs} (100%) rename wallet/src/{token_program_interactions.rs => program_interactions/token.rs} (100%) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index f36e589..4bdf37f 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -15,13 +15,15 @@ use sequencer_runner::startup_sequencer; use tempfile::TempDir; use tokio::task::JoinHandle; use wallet::{ - Command, SubcommandReturnValue, WalletCore, + WalletCore, cli::{ + Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, config::ConfigSubcommand, - native_token_transfer_program::AuthTransferSubcommand, - pinata_program::PinataProgramAgnosticSubcommand, - token_program::TokenProgramAgnosticSubcommand, + programs::{ + native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, + token::TokenProgramAgnosticSubcommand, + }, }, config::{PersistentAccountData, PersistentStorage}, helperfunctions::{fetch_config, fetch_persistent_storage}, @@ -56,7 +58,7 @@ pub fn prepare_function_map() -> HashMap { let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -89,7 +91,7 @@ pub fn prepare_function_map() -> HashMap { let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); let PersistentStorage { accounts: persistent_accounts, @@ -120,7 +122,7 @@ pub fn prepare_function_map() -> HashMap { amount: 100, }); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -159,7 +161,7 @@ pub fn prepare_function_map() -> HashMap { let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); - let failed_send = wallet::execute_subcommand(command).await; + let failed_send = wallet::cli::execute_subcommand(command).await; assert!(failed_send.is_err()); @@ -200,7 +202,7 @@ pub fn prepare_function_map() -> HashMap { let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -231,7 +233,7 @@ pub fn prepare_function_map() -> HashMap { amount: 100, }); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -284,19 +286,19 @@ pub fn prepare_function_map() -> HashMap { let wallet_config = fetch_config().await.unwrap(); // Create new account for the token definition - wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await .unwrap(); // Create new account for the token supply holder - wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await .unwrap(); // Create new account for receiving a token transaction - wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await @@ -339,7 +341,7 @@ pub fn prepare_function_map() -> HashMap { name: "A NAME".to_string(), total_supply: 37, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); info!("Waiting for next block creation"); @@ -398,7 +400,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); info!("Waiting for next block creation"); @@ -453,7 +455,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition (public) let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await @@ -464,7 +466,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private {}, ))) .await @@ -475,7 +477,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for receiving a token transaction let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private {}, ))) .await @@ -494,7 +496,7 @@ pub fn prepare_function_map() -> HashMap { total_supply: 37, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -541,7 +543,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -575,7 +577,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -608,7 +610,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition (public) let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await @@ -619,7 +621,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private {}, ))) .await @@ -630,7 +632,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for receiving a token transaction let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private {}, ))) .await @@ -649,7 +651,7 @@ pub fn prepare_function_map() -> HashMap { total_supply: 37, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -703,7 +705,7 @@ pub fn prepare_function_map() -> HashMap { }; let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash: _ } = - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap() else { @@ -715,7 +717,7 @@ pub fn prepare_function_map() -> HashMap { let command = Command::Account(AccountSubcommand::SyncPrivate {}); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); let wallet_config = fetch_config().await.unwrap(); let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) @@ -744,7 +746,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition (public) let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await @@ -755,7 +757,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (public) let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await @@ -766,7 +768,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for receiving a token transaction let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private {}, ))) .await @@ -785,7 +787,7 @@ pub fn prepare_function_map() -> HashMap { total_supply: 37, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -822,7 +824,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -851,7 +853,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -880,7 +882,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition (public) let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await @@ -891,7 +893,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private {}, ))) .await @@ -902,7 +904,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for receiving a token transaction let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public {}, ))) .await @@ -921,7 +923,7 @@ pub fn prepare_function_map() -> HashMap { total_supply: 37, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -968,7 +970,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -997,7 +999,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -1029,7 +1031,7 @@ pub fn prepare_function_map() -> HashMap { amount: 100, }); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -1068,7 +1070,7 @@ pub fn prepare_function_map() -> HashMap { }); let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = - wallet::execute_subcommand(command).await.unwrap() + wallet::cli::execute_subcommand(command).await.unwrap() else { panic!("invalid subcommand return value"); }; @@ -1106,7 +1108,7 @@ pub fn prepare_function_map() -> HashMap { let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); - let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); let SubcommandReturnValue::RegisterAccount { account_id: to_account_id, } = sub_ret @@ -1136,7 +1138,7 @@ pub fn prepare_function_map() -> HashMap { amount: 100, }); - let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else { panic!("FAILED TO SEND TX"); }; @@ -1144,7 +1146,7 @@ pub fn prepare_function_map() -> HashMap { let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; let command = Command::Account(AccountSubcommand::SyncPrivate {}); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) .await .unwrap(); @@ -1171,13 +1173,13 @@ pub fn prepare_function_map() -> HashMap { // info!( // "########## test_success_private_transfer_to_another_owned_account_cont_run_path // ##########" ); - // let continious_run_handle = tokio::spawn(wallet::execute_continious_run()); + // let continious_run_handle = tokio::spawn(wallet::cli::execute_continious_run()); // let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap(); // let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); - // let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + // let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); // let SubcommandReturnValue::RegisterAccount { // account_id: to_account_id, // } = sub_ret @@ -1207,7 +1209,7 @@ pub fn prepare_function_map() -> HashMap { // amount: 100, // }); - // let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + // let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); // let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else { // panic!("FAILED TO SEND TX"); // }; @@ -1258,7 +1260,7 @@ pub fn prepare_function_map() -> HashMap { let from_acc = wallet_storage.get_account_private(&from).unwrap(); assert_eq!(from_acc.balance, 10000); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -1301,7 +1303,7 @@ pub fn prepare_function_map() -> HashMap { let wallet_config = fetch_config().await.unwrap(); let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -1347,7 +1349,7 @@ pub fn prepare_function_map() -> HashMap { let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = - wallet::execute_subcommand(command).await.unwrap() + wallet::cli::execute_subcommand(command).await.unwrap() else { panic!("invalid subcommand return value"); }; @@ -1393,7 +1395,7 @@ pub fn prepare_function_map() -> HashMap { .unwrap() .balance; - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -1470,7 +1472,7 @@ pub fn prepare_function_map() -> HashMap { info!("########## test initialize account for authenticated transfer ##########"); let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {})); let SubcommandReturnValue::RegisterAccount { account_id } = - wallet::execute_subcommand(command).await.unwrap() + wallet::cli::execute_subcommand(command).await.unwrap() else { panic!("Error creating account"); }; @@ -1478,7 +1480,7 @@ pub fn prepare_function_map() -> HashMap { let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: make_public_account_input_from_str(&account_id.to_string()), }); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Checking correct execution"); let wallet_config = fetch_config().await.unwrap(); @@ -1524,7 +1526,7 @@ pub fn prepare_function_map() -> HashMap { .balance; let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash: _ } = - wallet::execute_subcommand(command).await.unwrap() + wallet::cli::execute_subcommand(command).await.unwrap() else { panic!("invalid subcommand return value"); }; @@ -1540,7 +1542,7 @@ pub fn prepare_function_map() -> HashMap { .balance; let command = Command::Account(AccountSubcommand::SyncPrivate {}); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); let wallet_config = fetch_config().await.unwrap(); let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); @@ -1568,7 +1570,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { account_id: winner_account_id, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private {}, ))) .await @@ -1592,7 +1594,7 @@ pub fn prepare_function_map() -> HashMap { .unwrap() .balance; - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -1632,7 +1634,7 @@ pub fn prepare_function_map() -> HashMap { key: "seq_poll_retry_delay_millis".to_string(), value: "1000".to_string(), }); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); let wallet_config = fetch_config().await.unwrap(); @@ -1643,7 +1645,7 @@ pub fn prepare_function_map() -> HashMap { key: "seq_poll_retry_delay_millis".to_string(), value: old_seq_poll_retry_delay_millis.to_string(), }); - wallet::execute_subcommand(command).await.unwrap(); + wallet::cli::execute_subcommand(command).await.unwrap(); info!("Success!"); } diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage.rs similarity index 100% rename from wallet/src/chain_storage/mod.rs rename to wallet/src/chain_storage.rs diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index d1e361a..e7e47b6 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -6,10 +6,11 @@ use nssa::{Account, AccountId, program::Program}; use serde::Serialize; use crate::{ - SubcommandReturnValue, WalletCore, - cli::WalletSubcommand, - helperfunctions::{AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix}, - parse_block_range, + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, + helperfunctions::{ + AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix, parse_block_range, + }, }; const TOKEN_DEFINITION_TYPE: u8 = 0; diff --git a/wallet/src/cli/chain.rs b/wallet/src/cli/chain.rs index a606066..419fa5e 100644 --- a/wallet/src/cli/chain.rs +++ b/wallet/src/cli/chain.rs @@ -1,7 +1,10 @@ use anyhow::Result; use clap::Subcommand; -use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand}; +use crate::{ + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, +}; /// Represents generic chain CLI subcommand #[derive(Subcommand, Debug, Clone)] diff --git a/wallet/src/cli/config.rs b/wallet/src/cli/config.rs index c41aa32..68670af 100644 --- a/wallet/src/cli/config.rs +++ b/wallet/src/cli/config.rs @@ -1,7 +1,10 @@ use anyhow::Result; use clap::Subcommand; -use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand}; +use crate::{ + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, +}; /// Represents generic config CLI subcommand #[derive(Subcommand, Debug, Clone)] diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index fff14cb..25c6819 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,15 +1,177 @@ -use anyhow::Result; +use std::sync::Arc; -use crate::{SubcommandReturnValue, WalletCore}; +use anyhow::Result; +use clap::{Parser, Subcommand}; +use common::sequencer_client::SequencerClient; +use nssa::program::Program; + +use crate::{ + WalletCore, + cli::{ + account::AccountSubcommand, + chain::ChainSubcommand, + config::ConfigSubcommand, + programs::{ + native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, + token::TokenProgramAgnosticSubcommand, + }, + }, + helperfunctions::{fetch_config, parse_block_range}, +}; pub mod account; pub mod chain; pub mod config; -pub mod native_token_transfer_program; -pub mod pinata_program; -pub mod token_program; +pub mod programs; pub(crate) trait WalletSubcommand { async fn handle_subcommand(self, wallet_core: &mut WalletCore) -> Result; } + +/// Represents CLI command for a wallet +#[derive(Subcommand, Debug, Clone)] +#[clap(about)] +pub enum Command { + /// Authenticated transfer subcommand + #[command(subcommand)] + AuthTransfer(AuthTransferSubcommand), + /// Generic chain info subcommand + #[command(subcommand)] + ChainInfo(ChainSubcommand), + /// Account view and sync subcommand + #[command(subcommand)] + Account(AccountSubcommand), + /// Pinata program interaction subcommand + #[command(subcommand)] + Pinata(PinataProgramAgnosticSubcommand), + /// Token program interaction subcommand + #[command(subcommand)] + Token(TokenProgramAgnosticSubcommand), + /// Check the wallet can connect to the node and builtin local programs + /// match the remote versions + CheckHealth {}, + /// Command to setup config, get and set config fields + #[command(subcommand)] + Config(ConfigSubcommand), +} + +/// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config +/// +/// All account adresses must be valid 32 byte base58 strings. +/// +/// All account account_ids must be provided as {privacy_prefix}/{account_id}, +/// where valid options for `privacy_prefix` is `Public` and `Private` +#[derive(Parser, Debug)] +#[clap(version, about)] +pub struct Args { + /// Continious run flag + #[arg(short, long)] + pub continuous_run: bool, + /// Wallet command + #[command(subcommand)] + pub command: Option, +} + +#[derive(Debug, Clone)] +pub enum SubcommandReturnValue { + PrivacyPreservingTransfer { tx_hash: String }, + RegisterAccount { account_id: nssa::AccountId }, + Account(nssa::Account), + Empty, + SyncedToBlock(u64), +} + +pub async fn execute_subcommand(command: Command) -> Result { + let wallet_config = fetch_config().await?; + let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?; + + let subcommand_ret = match command { + Command::AuthTransfer(transfer_subcommand) => { + transfer_subcommand + .handle_subcommand(&mut wallet_core) + .await? + } + Command::ChainInfo(chain_subcommand) => { + chain_subcommand.handle_subcommand(&mut wallet_core).await? + } + Command::Account(account_subcommand) => { + account_subcommand + .handle_subcommand(&mut wallet_core) + .await? + } + Command::Pinata(pinata_subcommand) => { + pinata_subcommand + .handle_subcommand(&mut wallet_core) + .await? + } + Command::CheckHealth {} => { + let remote_program_ids = wallet_core + .sequencer_client + .get_program_ids() + .await + .expect("Error fetching program ids"); + let Some(authenticated_transfer_id) = remote_program_ids.get("authenticated_transfer") + else { + panic!("Missing authenticated transfer ID from remote"); + }; + if authenticated_transfer_id != &Program::authenticated_transfer_program().id() { + panic!("Local ID for authenticated transfer program is different from remote"); + } + let Some(token_id) = remote_program_ids.get("token") else { + panic!("Missing token program ID from remote"); + }; + if token_id != &Program::token().id() { + panic!("Local ID for token program is different from remote"); + } + let Some(circuit_id) = remote_program_ids.get("privacy_preserving_circuit") else { + panic!("Missing privacy preserving circuit ID from remote"); + }; + if circuit_id != &nssa::PRIVACY_PRESERVING_CIRCUIT_ID { + panic!("Local ID for privacy preserving circuit is different from remote"); + } + + println!("✅All looks good!"); + + SubcommandReturnValue::Empty + } + Command::Token(token_subcommand) => { + token_subcommand.handle_subcommand(&mut wallet_core).await? + } + Command::Config(config_subcommand) => { + config_subcommand + .handle_subcommand(&mut wallet_core) + .await? + } + }; + + Ok(subcommand_ret) +} + +pub async fn execute_continuous_run() -> Result<()> { + let config = fetch_config().await?; + let seq_client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); + let mut wallet_core = WalletCore::start_from_config_update_chain(config.clone()).await?; + + let mut latest_block_num = seq_client.get_last_block().await?.last_block; + let mut curr_last_block = latest_block_num; + + loop { + parse_block_range( + curr_last_block, + latest_block_num, + seq_client.clone(), + &mut wallet_core, + ) + .await?; + + curr_last_block = latest_block_num + 1; + + tokio::time::sleep(std::time::Duration::from_millis( + config.seq_poll_timeout_millis, + )) + .await; + + latest_block_num = seq_client.get_last_block().await?.last_block; + } +} diff --git a/wallet/src/cli/programs/mod.rs b/wallet/src/cli/programs/mod.rs new file mode 100644 index 0000000..3ffb7bb --- /dev/null +++ b/wallet/src/cli/programs/mod.rs @@ -0,0 +1,3 @@ +pub mod native_token_transfer; +pub mod pinata; +pub mod token; diff --git a/wallet/src/cli/native_token_transfer_program.rs b/wallet/src/cli/programs/native_token_transfer.rs similarity index 99% rename from wallet/src/cli/native_token_transfer_program.rs rename to wallet/src/cli/programs/native_token_transfer.rs index f5fdb9a..2a9b4bf 100644 --- a/wallet/src/cli/native_token_transfer_program.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -4,8 +4,8 @@ use common::transaction::NSSATransaction; use nssa::AccountId; use crate::{ - SubcommandReturnValue, WalletCore, - cli::WalletSubcommand, + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, }; diff --git a/wallet/src/cli/pinata_program.rs b/wallet/src/cli/programs/pinata.rs similarity index 99% rename from wallet/src/cli/pinata_program.rs rename to wallet/src/cli/programs/pinata.rs index cc71a51..3d8dac3 100644 --- a/wallet/src/cli/pinata_program.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -4,8 +4,8 @@ use common::{PINATA_BASE58, transaction::NSSATransaction}; use log::info; use crate::{ - SubcommandReturnValue, WalletCore, - cli::WalletSubcommand, + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, }; diff --git a/wallet/src/cli/token_program.rs b/wallet/src/cli/programs/token.rs similarity index 99% rename from wallet/src/cli/token_program.rs rename to wallet/src/cli/programs/token.rs index b412e2f..a2a2d54 100644 --- a/wallet/src/cli/token_program.rs +++ b/wallet/src/cli/programs/token.rs @@ -4,8 +4,8 @@ use common::transaction::NSSATransaction; use nssa::AccountId; use crate::{ - SubcommandReturnValue, WalletCore, - cli::WalletSubcommand, + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, }; diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index e274876..ddb475e 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -1,16 +1,19 @@ -use std::{path::PathBuf, str::FromStr}; +use std::{path::PathBuf, str::FromStr, sync::Arc}; use anyhow::Result; use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; +use common::{ + block::HashableBlockData, sequencer_client::SequencerClient, transaction::NSSATransaction, +}; use key_protocol::key_protocol_core::NSSAUserData; -use nssa::Account; +use nssa::{Account, privacy_preserving_transaction::message::EncryptedAccountData}; use nssa_core::account::Nonce; use rand::{RngCore, rngs::OsRng}; use serde::Serialize; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ - HOME_DIR_ENV_VAR, + HOME_DIR_ENV_VAR, WalletCore, config::{ PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig, }, @@ -199,6 +202,80 @@ impl From for HumanReadableAccount { } } +pub async fn parse_block_range( + start: u64, + stop: u64, + seq_client: Arc, + wallet_core: &mut WalletCore, +) -> Result<()> { + for block_id in start..(stop + 1) { + let block = + borsh::from_slice::(&seq_client.get_block(block_id).await?.block)?; + + for tx in block.transactions { + let nssa_tx = NSSATransaction::try_from(&tx)?; + + if let NSSATransaction::PrivacyPreserving(tx) = nssa_tx { + let mut affected_accounts = vec![]; + + for (acc_account_id, (key_chain, _)) in + &wallet_core.storage.user_data.user_private_accounts + { + let view_tag = EncryptedAccountData::compute_view_tag( + key_chain.nullifer_public_key.clone(), + key_chain.incoming_viewing_public_key.clone(), + ); + + for (ciph_id, encrypted_data) in tx + .message() + .encrypted_private_post_states + .iter() + .enumerate() + { + if encrypted_data.view_tag == view_tag { + let ciphertext = &encrypted_data.ciphertext; + let commitment = &tx.message.new_commitments[ciph_id]; + let shared_secret = key_chain + .calculate_shared_secret_receiver(encrypted_data.epk.clone()); + + let res_acc = nssa_core::EncryptionScheme::decrypt( + ciphertext, + &shared_secret, + commitment, + ciph_id as u32, + ); + + if let Some(res_acc) = res_acc { + println!( + "Received new account for account_id {acc_account_id:#?} with account object {res_acc:#?}" + ); + + affected_accounts.push((*acc_account_id, res_acc)); + } + } + } + } + + for (affected_account_id, new_acc) in affected_accounts { + wallet_core + .storage + .insert_private_account_data(affected_account_id, new_acc); + } + } + } + + wallet_core.last_synced_block = block_id; + wallet_core.store_persistent_data().await?; + + println!( + "Block at id {block_id} with timestamp {} parsed", + block.timestamp + ); + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 7a58224..b769a24 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -3,30 +3,19 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::Result; use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; use chain_storage::WalletChainStore; -use clap::{Parser, Subcommand}; use common::{ - block::HashableBlockData, sequencer_client::SequencerClient, transaction::{EncodedTransaction, NSSATransaction}, }; use config::WalletConfig; use log::info; -use nssa::{ - Account, AccountId, privacy_preserving_transaction::message::EncryptedAccountData, - program::Program, -}; +use nssa::{Account, AccountId}; use nssa_core::{Commitment, MembershipProof}; use tokio::io::AsyncWriteExt; use crate::{ - cli::{ - WalletSubcommand, account::AccountSubcommand, chain::ChainSubcommand, - config::ConfigSubcommand, native_token_transfer_program::AuthTransferSubcommand, - pinata_program::PinataProgramAgnosticSubcommand, - token_program::TokenProgramAgnosticSubcommand, - }, config::PersistentStorage, - helperfunctions::{fetch_config, fetch_persistent_storage, get_home, produce_data_for_storage}, + helperfunctions::{fetch_persistent_storage, get_home, produce_data_for_storage}, poller::TxPoller, }; @@ -36,9 +25,8 @@ pub mod chain_storage; pub mod cli; pub mod config; pub mod helperfunctions; -pub mod pinata_interactions; pub mod poller; -pub mod token_program_interactions; +pub mod program_interactions; pub mod token_transfers; pub mod transaction_utils; @@ -209,224 +197,3 @@ impl WalletCore { Ok(()) } } - -/// Represents CLI command for a wallet -#[derive(Subcommand, Debug, Clone)] -#[clap(about)] -pub enum Command { - /// Authenticated transfer subcommand - #[command(subcommand)] - AuthTransfer(AuthTransferSubcommand), - /// Generic chain info subcommand - #[command(subcommand)] - ChainInfo(ChainSubcommand), - /// Account view and sync subcommand - #[command(subcommand)] - Account(AccountSubcommand), - /// Pinata program interaction subcommand - #[command(subcommand)] - Pinata(PinataProgramAgnosticSubcommand), - /// Token program interaction subcommand - #[command(subcommand)] - Token(TokenProgramAgnosticSubcommand), - /// Check the wallet can connect to the node and builtin local programs - /// match the remote versions - CheckHealth {}, - /// Command to setup config, get and set config fields - #[command(subcommand)] - Config(ConfigSubcommand), -} - -/// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config -/// -/// All account adresses must be valid 32 byte base58 strings. -/// -/// All account account_ids must be provided as {privacy_prefix}/{account_id}, -/// where valid options for `privacy_prefix` is `Public` and `Private` -#[derive(Parser, Debug)] -#[clap(version, about)] -pub struct Args { - /// Continious run flag - #[arg(short, long)] - pub continious_run: bool, - /// Wallet command - #[command(subcommand)] - pub command: Option, -} - -#[derive(Debug, Clone)] -pub enum SubcommandReturnValue { - PrivacyPreservingTransfer { tx_hash: String }, - RegisterAccount { account_id: nssa::AccountId }, - Account(nssa::Account), - Empty, - SyncedToBlock(u64), -} - -pub async fn execute_subcommand(command: Command) -> Result { - let wallet_config = fetch_config().await?; - let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?; - - let subcommand_ret = match command { - Command::AuthTransfer(transfer_subcommand) => { - transfer_subcommand - .handle_subcommand(&mut wallet_core) - .await? - } - Command::ChainInfo(chain_subcommand) => { - chain_subcommand.handle_subcommand(&mut wallet_core).await? - } - Command::Account(account_subcommand) => { - account_subcommand - .handle_subcommand(&mut wallet_core) - .await? - } - Command::Pinata(pinata_subcommand) => { - pinata_subcommand - .handle_subcommand(&mut wallet_core) - .await? - } - Command::CheckHealth {} => { - let remote_program_ids = wallet_core - .sequencer_client - .get_program_ids() - .await - .expect("Error fetching program ids"); - let Some(authenticated_transfer_id) = remote_program_ids.get("authenticated_transfer") - else { - panic!("Missing authenticated transfer ID from remote"); - }; - if authenticated_transfer_id != &Program::authenticated_transfer_program().id() { - panic!("Local ID for authenticated transfer program is different from remote"); - } - let Some(token_id) = remote_program_ids.get("token") else { - panic!("Missing token program ID from remote"); - }; - if token_id != &Program::token().id() { - panic!("Local ID for token program is different from remote"); - } - let Some(circuit_id) = remote_program_ids.get("privacy_preserving_circuit") else { - panic!("Missing privacy preserving circuit ID from remote"); - }; - if circuit_id != &nssa::PRIVACY_PRESERVING_CIRCUIT_ID { - panic!("Local ID for privacy preserving circuit is different from remote"); - } - - println!("✅All looks good!"); - - SubcommandReturnValue::Empty - } - Command::Token(token_subcommand) => { - token_subcommand.handle_subcommand(&mut wallet_core).await? - } - Command::Config(config_subcommand) => { - config_subcommand - .handle_subcommand(&mut wallet_core) - .await? - } - }; - - Ok(subcommand_ret) -} - -pub async fn parse_block_range( - start: u64, - stop: u64, - seq_client: Arc, - wallet_core: &mut WalletCore, -) -> Result<()> { - for block_id in start..(stop + 1) { - let block = - borsh::from_slice::(&seq_client.get_block(block_id).await?.block)?; - - for tx in block.transactions { - let nssa_tx = NSSATransaction::try_from(&tx)?; - - if let NSSATransaction::PrivacyPreserving(tx) = nssa_tx { - let mut affected_accounts = vec![]; - - for (acc_account_id, (key_chain, _)) in - &wallet_core.storage.user_data.user_private_accounts - { - let view_tag = EncryptedAccountData::compute_view_tag( - key_chain.nullifer_public_key.clone(), - key_chain.incoming_viewing_public_key.clone(), - ); - - for (ciph_id, encrypted_data) in tx - .message() - .encrypted_private_post_states - .iter() - .enumerate() - { - if encrypted_data.view_tag == view_tag { - let ciphertext = &encrypted_data.ciphertext; - let commitment = &tx.message.new_commitments[ciph_id]; - let shared_secret = key_chain - .calculate_shared_secret_receiver(encrypted_data.epk.clone()); - - let res_acc = nssa_core::EncryptionScheme::decrypt( - ciphertext, - &shared_secret, - commitment, - ciph_id as u32, - ); - - if let Some(res_acc) = res_acc { - println!( - "Received new account for account_id {acc_account_id:#?} with account object {res_acc:#?}" - ); - - affected_accounts.push((*acc_account_id, res_acc)); - } - } - } - } - - for (affected_account_id, new_acc) in affected_accounts { - wallet_core - .storage - .insert_private_account_data(affected_account_id, new_acc); - } - } - } - - wallet_core.last_synced_block = block_id; - wallet_core.store_persistent_data().await?; - - println!( - "Block at id {block_id} with timestamp {} parsed", - block.timestamp - ); - } - - Ok(()) -} - -pub async fn execute_continious_run() -> Result<()> { - let config = fetch_config().await?; - let seq_client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); - let mut wallet_core = WalletCore::start_from_config_update_chain(config.clone()).await?; - - let mut latest_block_num = seq_client.get_last_block().await?.last_block; - let mut curr_last_block = latest_block_num; - - loop { - parse_block_range( - curr_last_block, - latest_block_num, - seq_client.clone(), - &mut wallet_core, - ) - .await?; - - curr_last_block = latest_block_num + 1; - - tokio::time::sleep(std::time::Duration::from_millis( - config.seq_poll_timeout_millis, - )) - .await; - - latest_block_num = seq_client.get_last_block().await?.last_block; - } -} diff --git a/wallet/src/main.rs b/wallet/src/main.rs index d38af75..0138b92 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -1,14 +1,16 @@ use anyhow::Result; -use clap::{CommandFactory, Parser}; +use clap::{CommandFactory as _, Parser as _}; use tokio::runtime::Builder; -use wallet::{Args, execute_continious_run, execute_subcommand}; +use wallet::cli::{Args, execute_continuous_run, execute_subcommand}; pub const NUM_THREADS: usize = 2; // TODO #169: We have sample configs for sequencer, but not for wallet // TODO #168: Why it requires config as a directory? Maybe better to deduce directory from config -// file path? TODO #172: Why it requires config as env var while sequencer_runner accepts as -// argument? TODO #171: Running pinata doesn't give output about transaction hash and etc. +// file path? +// TODO #172: Why it requires config as env var while sequencer_runner accepts as +// argument? +// TODO #171: Running pinata doesn't give output about transaction hash and etc. fn main() -> Result<()> { let runtime = Builder::new_multi_thread() .worker_threads(NUM_THREADS) @@ -22,15 +24,15 @@ fn main() -> Result<()> { runtime.block_on(async move { if let Some(command) = args.command { - // TODO: It should return error, not panic - execute_subcommand(command).await.unwrap(); - } else if args.continious_run { - execute_continious_run().await.unwrap(); + // TODO: Do something with the output + let _output = execute_subcommand(command).await?; + Ok(()) + } else if args.continuous_run { + execute_continuous_run().await } else { let help = Args::command().render_long_help(); println!("{help}"); + Ok(()) } - }); - - Ok(()) + }) } diff --git a/wallet/src/program_interactions/mod.rs b/wallet/src/program_interactions/mod.rs new file mode 100644 index 0000000..fbdd6ab --- /dev/null +++ b/wallet/src/program_interactions/mod.rs @@ -0,0 +1,2 @@ +pub mod pinata; +pub mod token; diff --git a/wallet/src/pinata_interactions.rs b/wallet/src/program_interactions/pinata.rs similarity index 100% rename from wallet/src/pinata_interactions.rs rename to wallet/src/program_interactions/pinata.rs diff --git a/wallet/src/token_program_interactions.rs b/wallet/src/program_interactions/token.rs similarity index 100% rename from wallet/src/token_program_interactions.rs rename to wallet/src/program_interactions/token.rs From 41a833c25826e4dd981f49af0e4279a8109444c6 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 01:19:06 +0300 Subject: [PATCH 23/70] refactor: implement universal interface for privacy-preserving transactions --- .../src/cli/programs/native_token_transfer.rs | 38 +- wallet/src/cli/programs/token.rs | 62 +-- wallet/src/lib.rs | 85 ++- wallet/src/privacy_preserving_tx.rs | 173 +++++++ wallet/src/program_interactions/token.rs | 164 +++--- wallet/src/token_transfers/deshielded.rs | 23 +- wallet/src/token_transfers/mod.rs | 5 +- wallet/src/token_transfers/private.rs | 57 +- wallet/src/token_transfers/shielded.rs | 64 ++- wallet/src/transaction_utils.rs | 487 +----------------- 10 files changed, 460 insertions(+), 698 deletions(-) create mode 100644 wallet/src/privacy_preserving_tx.rs diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 2a9b4bf..3b1f2ae 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -317,21 +317,9 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); - let to_initialization = wallet_core.check_private_account_initialized(&to).await?; - - let (res, [secret_from, secret_to]) = if let Some(to_proof) = to_initialization { - wallet_core - .send_private_native_token_transfer_owned_account_already_initialized( - from, to, amount, to_proof, - ) - .await? - } else { - wallet_core - .send_private_native_token_transfer_owned_account_not_initialized( - from, to, amount, - ) - .await? - }; + let (res, [secret_from, secret_to]) = wallet_core + .send_private_native_token_transfer_owned_account(from, to, amount) + .await?; println!("Results of tx send is {res:#?}"); @@ -413,19 +401,9 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); - let to_initialization = wallet_core.check_private_account_initialized(&to).await?; - - let (res, [secret]) = if let Some(to_proof) = to_initialization { - wallet_core - .send_shielded_native_token_transfer_already_initialized( - from, to, amount, to_proof, - ) - .await? - } else { - wallet_core - .send_shielded_native_token_transfer_not_initialized(from, to, amount) - .await? - }; + let (res, secret) = wallet_core + .send_shielded_native_token_transfer(from, to, amount) + .await?; println!("Results of tx send is {res:#?}"); @@ -468,7 +446,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { let to_ipk = nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_ipk.to_vec()); - let res = wallet_core + let (res, _) = wallet_core .send_shielded_native_token_transfer_outer_account(from, to_npk, to_ipk, amount) .await?; @@ -502,7 +480,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); - let (res, [secret]) = wallet_core + let (res, secret) = wallet_core .send_deshielded_native_token_transfer(from, to, amount) .await?; diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index a2a2d54..0671e4b 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -389,7 +389,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { let definition_account_id: AccountId = definition_account_id.parse().unwrap(); let supply_account_id: AccountId = supply_account_id.parse().unwrap(); - let (res, [secret_supply]) = wallet_core + let (res, secret_supply) = wallet_core .send_new_token_definition_private_owned( definition_account_id, supply_account_id, @@ -428,30 +428,14 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { let sender_account_id: AccountId = sender_account_id.parse().unwrap(); let recipient_account_id: AccountId = recipient_account_id.parse().unwrap(); - let recipient_initialization = wallet_core - .check_private_account_initialized(&recipient_account_id) + let (res, [secret_sender, secret_recipient]) = wallet_core + .send_transfer_token_transaction_private_owned_account( + sender_account_id, + recipient_account_id, + balance_to_move, + ) .await?; - let (res, [secret_sender, secret_recipient]) = - if let Some(recipient_proof) = recipient_initialization { - wallet_core - .send_transfer_token_transaction_private_owned_account_already_initialized( - sender_account_id, - recipient_account_id, - balance_to_move, - recipient_proof, - ) - .await? - } else { - wallet_core - .send_transfer_token_transaction_private_owned_account_not_initialized( - sender_account_id, - recipient_account_id, - balance_to_move, - ) - .await? - }; - println!("Results of tx send is {res:#?}"); let tx_hash = res.tx_hash; @@ -545,7 +529,7 @@ impl WalletSubcommand for TokenProgramSubcommandDeshielded { let sender_account_id: AccountId = sender_account_id.parse().unwrap(); let recipient_account_id: AccountId = recipient_account_id.parse().unwrap(); - let (res, [secret_sender]) = wallet_core + let (res, secret_sender) = wallet_core .send_transfer_token_transaction_deshielded( sender_account_id, recipient_account_id, @@ -604,7 +588,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { recipient_ipk.to_vec(), ); - let res = wallet_core + let (res, _) = wallet_core .send_transfer_token_transaction_shielded_foreign_account( sender_account_id, recipient_npk, @@ -638,30 +622,14 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { let sender_account_id: AccountId = sender_account_id.parse().unwrap(); let recipient_account_id: AccountId = recipient_account_id.parse().unwrap(); - let recipient_initialization = wallet_core - .check_private_account_initialized(&recipient_account_id) + let (res, secret_recipient) = wallet_core + .send_transfer_token_transaction_shielded_owned_account( + sender_account_id, + recipient_account_id, + balance_to_move, + ) .await?; - let (res, [secret_recipient]) = - if let Some(recipient_proof) = recipient_initialization { - wallet_core - .send_transfer_token_transaction_shielded_owned_account_already_initialized( - sender_account_id, - recipient_account_id, - balance_to_move, - recipient_proof, - ) - .await? - } else { - wallet_core - .send_transfer_token_transaction_shielded_owned_account_not_initialized( - sender_account_id, - recipient_account_id, - balance_to_move, - ) - .await? - }; - println!("Results of tx send is {res:#?}"); let tx_hash = res.tx_hash; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index b769a24..7a009c3 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -4,18 +4,22 @@ use anyhow::Result; use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; use chain_storage::WalletChainStore; use common::{ - sequencer_client::SequencerClient, + error::ExecutionFailureKind, + sequencer_client::{SequencerClient, json::SendTxResponse}, transaction::{EncodedTransaction, NSSATransaction}, }; use config::WalletConfig; use log::info; -use nssa::{Account, AccountId}; -use nssa_core::{Commitment, MembershipProof}; +use nssa::{Account, AccountId, PrivacyPreservingTransaction, program::Program}; +use nssa_core::{Commitment, MembershipProof, SharedSecretKey, program::InstructionData}; +pub use privacy_preserving_tx::PrivacyPreservingAccount; use tokio::io::AsyncWriteExt; use crate::{ config::PersistentStorage, - helperfunctions::{fetch_persistent_storage, get_home, produce_data_for_storage}, + helperfunctions::{ + fetch_persistent_storage, get_home, produce_data_for_storage, produce_random_nonces, + }, poller::TxPoller, }; @@ -26,6 +30,7 @@ pub mod cli; pub mod config; pub mod helperfunctions; pub mod poller; +mod privacy_preserving_tx; pub mod program_interactions; pub mod token_transfers; pub mod transaction_utils; @@ -129,6 +134,15 @@ impl WalletCore { Ok(response.account) } + pub fn get_account_public_signing_key( + &self, + account_id: &AccountId, + ) -> Option<&nssa::PrivateKey> { + self.storage + .user_data + .get_pub_account_signing_key(account_id) + } + pub fn get_account_private(&self, account_id: &AccountId) -> Option { self.storage .user_data @@ -196,4 +210,67 @@ impl WalletCore { Ok(()) } + + pub async fn send_privacy_preserving_tx( + &self, + accounts: Vec, + instruction_data: InstructionData, + tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, + program: Program, + ) -> Result<(SendTxResponse, Vec), ExecutionFailureKind> { + let payload = privacy_preserving_tx::Payload::new(self, accounts).await?; + + let pre_states = payload.pre_states(); + tx_pre_check( + &pre_states + .iter() + .map(|pre| &pre.account) + .collect::>(), + )?; + + let private_account_keys = payload.private_account_keys(); + let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( + &pre_states, + &instruction_data, + payload.visibility_mask(), + &produce_random_nonces(private_account_keys.len()), + &private_account_keys + .iter() + .map(|keys| (keys.npk.clone(), keys.ssk.clone())) + .collect::>(), + &payload.private_account_auth(), + &program, + ) + .unwrap(); + + let message = + nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( + payload.public_account_ids(), + Vec::from_iter(payload.public_account_nonces()), + private_account_keys + .iter() + .map(|keys| (keys.npk.clone(), keys.ipk.clone(), keys.epk.clone())) + .collect(), + output, + ) + .unwrap(); + + let witness_set = + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &payload.witness_signing_keys(), + ); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + let shared_secrets = private_account_keys + .into_iter() + .map(|keys| keys.ssk) + .collect(); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + shared_secrets, + )) + } } diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs new file mode 100644 index 0000000..be96a7a --- /dev/null +++ b/wallet/src/privacy_preserving_tx.rs @@ -0,0 +1,173 @@ +use common::error::ExecutionFailureKind; +use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; +use nssa::{AccountId, PrivateKey}; +use nssa_core::{ + MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + account::{AccountWithMetadata, Nonce}, + encryption::{EphemeralPublicKey, IncomingViewingPublicKey}, +}; + +use crate::{WalletCore, transaction_utils::AccountPreparedData}; + +pub enum PrivacyPreservingAccount { + Public(AccountId), + PrivateLocal(AccountId), + PrivateForeign { + npk: NullifierPublicKey, + ipk: IncomingViewingPublicKey, + }, +} + +pub struct PrivateAccountKeys { + pub npk: NullifierPublicKey, + pub ssk: SharedSecretKey, + pub ipk: IncomingViewingPublicKey, + pub epk: EphemeralPublicKey, +} + +enum State { + Public { + account: AccountWithMetadata, + sk: Option, + }, + Private(AccountPreparedData), +} + +pub struct Payload { + states: Vec, + visibility_mask: Vec, +} + +impl Payload { + pub async fn new( + wallet: &WalletCore, + accounts: Vec, + ) -> Result { + let mut pre_states = Vec::with_capacity(accounts.len()); + let mut visibility_mask = Vec::with_capacity(accounts.len()); + + for account in accounts { + let (state, mask) = match account { + PrivacyPreservingAccount::Public(account_id) => { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(|_| ExecutionFailureKind::KeyNotFoundError)?; + + let sk = wallet.get_account_public_signing_key(&account_id).cloned(); + let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); + + (State::Public { account, sk }, 0) + } + PrivacyPreservingAccount::PrivateLocal(account_id) => { + let mut pre = wallet + .private_acc_preparation(account_id, true, true) + .await?; + let mut mask = 1; + + if pre.proof.is_none() { + pre.auth_acc.is_authorized = false; + pre.nsk = None; + mask = 2 + }; + + (State::Private(pre), mask) + } + PrivacyPreservingAccount::PrivateForeign { npk, ipk } => { + let acc = nssa_core::account::Account::default(); + let auth_acc = AccountWithMetadata::new(acc, false, &npk); + let pre = AccountPreparedData { + nsk: None, + npk, + ipk, + auth_acc, + proof: None, + }; + + (State::Private(pre), 2) + } + }; + + pre_states.push(state); + visibility_mask.push(mask); + } + + Ok(Self { + states: pre_states, + visibility_mask, + }) + } + + pub fn pre_states(&self) -> Vec { + self.states + .iter() + .map(|state| match state { + State::Public { account, .. } => account.clone(), + State::Private(pre) => pre.auth_acc.clone(), + }) + .collect() + } + + pub fn visibility_mask(&self) -> &[u8] { + &self.visibility_mask + } + + pub fn public_account_nonces(&self) -> Vec { + self.states + .iter() + .filter_map(|state| match state { + State::Public { account, .. } => Some(account.account.nonce), + _ => None, + }) + .collect() + } + + pub fn private_account_keys(&self) -> Vec { + self.states + .iter() + .filter_map(|state| match state { + State::Private(pre) => { + let eph_holder = EphemeralKeyHolder::new(&pre.npk); + + Some(PrivateAccountKeys { + npk: pre.npk.clone(), + ssk: eph_holder.calculate_shared_secret_sender(&pre.ipk), + ipk: pre.ipk.clone(), + epk: eph_holder.generate_ephemeral_public_key(), + }) + } + _ => None, + }) + .collect() + } + + pub fn private_account_auth(&self) -> Vec<(NullifierSecretKey, MembershipProof)> { + self.states + .iter() + .filter_map(|state| match state { + State::Private(pre) => Some((pre.nsk?, pre.proof.clone()?)), + _ => None, + }) + .collect() + } + + pub fn public_account_ids(&self) -> Vec { + self.states + .iter() + .filter_map(|state| match state { + State::Public { account, .. } => Some(account.account_id), + _ => None, + }) + .collect() + } + + pub fn witness_signing_keys(&self) -> Vec<&PrivateKey> { + self.states + .iter() + .filter_map(|state| match state { + State::Public { sk, .. } => sk.as_ref(), + _ => None, + }) + .collect() + } +} diff --git a/wallet/src/program_interactions/token.rs b/wallet/src/program_interactions/token.rs index c441842..91f76d4 100644 --- a/wallet/src/program_interactions/token.rs +++ b/wallet/src/program_interactions/token.rs @@ -1,11 +1,11 @@ use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; use nssa::{Account, AccountId, program::Program}; use nssa_core::{ - MembershipProof, NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey, + NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey, program::InstructionData, }; -use crate::WalletCore; +use crate::{PrivacyPreservingAccount, WalletCore}; impl WalletCore { pub fn token_program_preparation_transfer( @@ -13,7 +13,7 @@ impl WalletCore { ) -> ( InstructionData, Program, - impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, ) { // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || // 0x00 || 0x00 || 0x00]. @@ -22,7 +22,7 @@ impl WalletCore { instruction[1..17].copy_from_slice(&amount.to_le_bytes()); let instruction_data = Program::serialize_instruction(instruction).unwrap(); let program = Program::token(); - let tx_pre_check = |_: &Account, _: &Account| Ok(()); + let tx_pre_check = |_: &[&Account]| Ok(()); (instruction_data, program, tx_pre_check) } @@ -33,7 +33,7 @@ impl WalletCore { ) -> ( InstructionData, Program, - impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, ) { // Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] let mut instruction = [0; 23]; @@ -41,7 +41,7 @@ impl WalletCore { instruction[17..].copy_from_slice(&name); let instruction_data = Program::serialize_instruction(instruction).unwrap(); let program = Program::token(); - let tx_pre_check = |_: &Account, _: &Account| Ok(()); + let tx_pre_check = |_: &[&Account]| Ok(()); (instruction_data, program, tx_pre_check) } @@ -80,20 +80,27 @@ impl WalletCore { supply_account_id: AccountId, name: [u8; 6], total_supply: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = WalletCore::token_program_preparation_definition(name, total_supply); - // Kind of non-obvious naming - // Basically this funtion is called because authentication mask is [0, 2] - self.shielded_two_accs_receiver_uninit( - definition_account_id, - supply_account_id, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(definition_account_id), + PrivacyPreservingAccount::PrivateLocal(supply_account_id), + ], instruction_data, tx_pre_check, program, ) .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected recipient's secret"); + (resp, first) + }) } pub async fn send_transfer_token_transaction( @@ -135,28 +142,7 @@ impl WalletCore { Ok(self.sequencer_client.send_tx_public(tx).await?) } - pub async fn send_transfer_token_transaction_private_owned_account_already_initialized( - &self, - sender_account_id: AccountId, - recipient_account_id: AccountId, - amount: u128, - recipient_proof: MembershipProof, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::token_program_preparation_transfer(amount); - - self.private_tx_two_accs_all_init( - sender_account_id, - recipient_account_id, - instruction_data, - tx_pre_check, - program, - recipient_proof, - ) - .await - } - - pub async fn send_transfer_token_transaction_private_owned_account_not_initialized( + pub async fn send_transfer_token_transaction_private_owned_account( &self, sender_account_id: AccountId, recipient_account_id: AccountId, @@ -165,14 +151,22 @@ impl WalletCore { let (instruction_data, program, tx_pre_check) = WalletCore::token_program_preparation_transfer(amount); - self.private_tx_two_accs_receiver_uninit( - sender_account_id, - recipient_account_id, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateLocal(sender_account_id), + PrivacyPreservingAccount::PrivateLocal(recipient_account_id), + ], instruction_data, tx_pre_check, program, ) .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected sender's secret"); + let second = iter.next().expect("expected recipient's secret"); + (resp, [first, second]) + }) } pub async fn send_transfer_token_transaction_private_foreign_account( @@ -185,15 +179,25 @@ impl WalletCore { let (instruction_data, program, tx_pre_check) = WalletCore::token_program_preparation_transfer(amount); - self.private_tx_two_accs_receiver_outer( - sender_account_id, - recipient_npk, - recipient_ipk, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateLocal(sender_account_id), + PrivacyPreservingAccount::PrivateForeign { + npk: recipient_npk, + ipk: recipient_ipk, + }, + ], instruction_data, tx_pre_check, program, ) .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected sender's secret"); + let second = iter.next().expect("expected recipient's secret"); + (resp, [first, second]) + }) } pub async fn send_transfer_token_transaction_deshielded( @@ -201,58 +205,55 @@ impl WalletCore { sender_account_id: AccountId, recipient_account_id: AccountId, amount: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = WalletCore::token_program_preparation_transfer(amount); - self.deshielded_tx_two_accs( - sender_account_id, - recipient_account_id, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateLocal(sender_account_id), + PrivacyPreservingAccount::Public(recipient_account_id), + ], instruction_data, tx_pre_check, program, ) .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected sender's secret"); + (resp, first) + }) } - pub async fn send_transfer_token_transaction_shielded_owned_account_already_initialized( + pub async fn send_transfer_token_transaction_shielded_owned_account( &self, sender_account_id: AccountId, recipient_account_id: AccountId, amount: u128, - recipient_proof: MembershipProof, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = WalletCore::token_program_preparation_transfer(amount); - self.shielded_two_accs_all_init( - sender_account_id, - recipient_account_id, - instruction_data, - tx_pre_check, - program, - recipient_proof, - ) - .await - } - - pub async fn send_transfer_token_transaction_shielded_owned_account_not_initialized( - &self, - sender_account_id: AccountId, - recipient_account_id: AccountId, - amount: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::token_program_preparation_transfer(amount); - - self.shielded_two_accs_receiver_uninit( - sender_account_id, - recipient_account_id, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(sender_account_id), + PrivacyPreservingAccount::PrivateLocal(recipient_account_id), + ], instruction_data, tx_pre_check, program, ) .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected recipient's secret"); + (resp, first) + }) } pub async fn send_transfer_token_transaction_shielded_foreign_account( @@ -261,18 +262,29 @@ impl WalletCore { recipient_npk: NullifierPublicKey, recipient_ipk: IncomingViewingPublicKey, amount: u128, - ) -> Result { + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = WalletCore::token_program_preparation_transfer(amount); - self.shielded_two_accs_receiver_outer( - sender_account_id, - recipient_npk, - recipient_ipk, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(sender_account_id), + PrivacyPreservingAccount::PrivateForeign { + npk: recipient_npk, + ipk: recipient_ipk, + }, + ], instruction_data, tx_pre_check, program, ) .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected recipient's secret"); + (resp, first) + }) } } diff --git a/wallet/src/token_transfers/deshielded.rs b/wallet/src/token_transfers/deshielded.rs index 4c8cbe3..216bfb5 100644 --- a/wallet/src/token_transfers/deshielded.rs +++ b/wallet/src/token_transfers/deshielded.rs @@ -1,7 +1,7 @@ use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; use nssa::AccountId; -use crate::WalletCore; +use crate::{PrivacyPreservingAccount, WalletCore}; impl WalletCore { pub async fn send_deshielded_native_token_transfer( @@ -9,11 +9,26 @@ impl WalletCore { from: AccountId, to: AccountId, balance_to_move: u128, - ) -> Result<(SendTxResponse, [nssa_core::SharedSecretKey; 1]), ExecutionFailureKind> { + ) -> Result<(SendTxResponse, nssa_core::SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = WalletCore::auth_transfer_preparation(balance_to_move); - self.deshielded_tx_two_accs(from, to, instruction_data, tx_pre_check, program) - .await + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateLocal(from), + PrivacyPreservingAccount::Public(to), + ], + instruction_data, + tx_pre_check, + program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected sender's secret"); + (resp, first) + }) } } diff --git a/wallet/src/token_transfers/mod.rs b/wallet/src/token_transfers/mod.rs index a785763..6b09698 100644 --- a/wallet/src/token_transfers/mod.rs +++ b/wallet/src/token_transfers/mod.rs @@ -15,11 +15,12 @@ impl WalletCore { ) -> ( InstructionData, Program, - impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, ) { let instruction_data = Program::serialize_instruction(balance_to_move).unwrap(); let program = Program::authenticated_transfer_program(); - let tx_pre_check = move |from: &Account, _: &Account| { + let tx_pre_check = move |accounts: &[&Account]| { + let from = accounts[0]; if from.balance >= balance_to_move { Ok(()) } else { diff --git a/wallet/src/token_transfers/private.rs b/wallet/src/token_transfers/private.rs index 35d3e3b..59af480 100644 --- a/wallet/src/token_transfers/private.rs +++ b/wallet/src/token_transfers/private.rs @@ -1,10 +1,10 @@ +use std::vec; + use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; use nssa::AccountId; -use nssa_core::{ - MembershipProof, NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey, -}; +use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; -use crate::WalletCore; +use crate::{PrivacyPreservingAccount, WalletCore}; impl WalletCore { pub async fn send_private_native_token_transfer_outer_account( @@ -17,18 +17,28 @@ impl WalletCore { let (instruction_data, program, tx_pre_check) = WalletCore::auth_transfer_preparation(balance_to_move); - self.private_tx_two_accs_receiver_outer( - from, - to_npk, - to_ipk, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateLocal(from), + PrivacyPreservingAccount::PrivateForeign { + npk: to_npk, + ipk: to_ipk, + }, + ], instruction_data, tx_pre_check, program, ) .await + .map(|(resp, secrets)| { + let mut secrets_iter = secrets.into_iter(); + let first = secrets_iter.next().expect("expected sender's secret"); + let second = secrets_iter.next().expect("expected receiver's secret"); + (resp, [first, second]) + }) } - pub async fn send_private_native_token_transfer_owned_account_not_initialized( + pub async fn send_private_native_token_transfer_owned_account( &self, from: AccountId, to: AccountId, @@ -37,28 +47,21 @@ impl WalletCore { let (instruction_data, program, tx_pre_check) = WalletCore::auth_transfer_preparation(balance_to_move); - self.private_tx_two_accs_receiver_uninit(from, to, instruction_data, tx_pre_check, program) - .await - } - - pub async fn send_private_native_token_transfer_owned_account_already_initialized( - &self, - from: AccountId, - to: AccountId, - balance_to_move: u128, - to_proof: MembershipProof, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::auth_transfer_preparation(balance_to_move); - - self.private_tx_two_accs_all_init( - from, - to, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateLocal(from), + PrivacyPreservingAccount::PrivateLocal(to), + ], instruction_data, tx_pre_check, program, - to_proof, ) .await + .map(|(resp, secrets)| { + let mut secrets_iter = secrets.into_iter(); + let first = secrets_iter.next().expect("expected sender's secret"); + let second = secrets_iter.next().expect("expected receiver's secret"); + (resp, [first, second]) + }) } } diff --git a/wallet/src/token_transfers/shielded.rs b/wallet/src/token_transfers/shielded.rs index 8ba260c..a8d28ee 100644 --- a/wallet/src/token_transfers/shielded.rs +++ b/wallet/src/token_transfers/shielded.rs @@ -1,37 +1,36 @@ use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; use nssa::AccountId; -use nssa_core::{ - MembershipProof, NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey, -}; +use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; -use crate::WalletCore; +use crate::{PrivacyPreservingAccount, WalletCore}; impl WalletCore { - pub async fn send_shielded_native_token_transfer_already_initialized( + pub async fn send_shielded_native_token_transfer( &self, from: AccountId, to: AccountId, balance_to_move: u128, - to_proof: MembershipProof, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = WalletCore::auth_transfer_preparation(balance_to_move); - self.shielded_two_accs_all_init(from, to, instruction_data, tx_pre_check, program, to_proof) - .await - } - - pub async fn send_shielded_native_token_transfer_not_initialized( - &self, - from: AccountId, - to: AccountId, - balance_to_move: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::auth_transfer_preparation(balance_to_move); - - self.shielded_two_accs_receiver_uninit(from, to, instruction_data, tx_pre_check, program) - .await + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(from), + PrivacyPreservingAccount::PrivateLocal(to), + ], + instruction_data, + tx_pre_check, + program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected sender's secret"); + (resp, first) + }) } pub async fn send_shielded_native_token_transfer_outer_account( @@ -40,18 +39,29 @@ impl WalletCore { to_npk: NullifierPublicKey, to_ipk: IncomingViewingPublicKey, balance_to_move: u128, - ) -> Result { + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = WalletCore::auth_transfer_preparation(balance_to_move); - self.shielded_two_accs_receiver_outer( - from, - to_npk, - to_ipk, + self.send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(from), + PrivacyPreservingAccount::PrivateForeign { + npk: to_npk, + ipk: to_ipk, + }, + ], instruction_data, tx_pre_check, program, ) .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected sender's secret"); + (resp, first) + }) } } diff --git a/wallet/src/transaction_utils.rs b/wallet/src/transaction_utils.rs index a54f81c..10854d9 100644 --- a/wallet/src/transaction_utils.rs +++ b/wallet/src/transaction_utils.rs @@ -1,13 +1,13 @@ use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use nssa::{ - Account, AccountId, PrivacyPreservingTransaction, + AccountId, PrivacyPreservingTransaction, privacy_preserving_transaction::{circuit, message::Message, witness_set::WitnessSet}, program::Program, }; use nssa_core::{ - Commitment, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - account::AccountWithMetadata, encryption::IncomingViewingPublicKey, program::InstructionData, + MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + account::AccountWithMetadata, encryption::IncomingViewingPublicKey, }; use crate::{WalletCore, helperfunctions::produce_random_nonces}; @@ -42,8 +42,6 @@ impl WalletCore { let from_npk = from_keys.nullifer_public_key; let from_ipk = from_keys.incoming_viewing_public_key; - let sender_commitment = Commitment::new(&from_npk, &from_acc); - let sender_pre = AccountWithMetadata::new(from_acc.clone(), is_authorized, &from_npk); if is_authorized { @@ -51,9 +49,9 @@ impl WalletCore { } if needs_proof { + // TODO: Remove this unwrap, error types must be compatible proof = self - .sequencer_client - .get_proof_for_commitment(sender_commitment) + .check_private_account_initialized(&account_id) .await .unwrap(); } @@ -67,480 +65,7 @@ impl WalletCore { }) } - pub(crate) async fn private_tx_two_accs_all_init( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - to_proof: MembershipProof, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: from_nsk, - npk: from_npk, - ipk: from_ipk, - auth_acc: sender_pre, - proof: from_proof, - } = self.private_acc_preparation(from, true, true).await?; - - let AccountPreparedData { - nsk: to_nsk, - npk: to_npk, - ipk: to_ipk, - auth_acc: recipient_pre, - proof: _, - } = self.private_acc_preparation(to, true, false).await?; - - tx_pre_check(&sender_pre.account, &recipient_pre.account)?; - - let eph_holder_from = EphemeralKeyHolder::new(&from_npk); - let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_ipk); - - let eph_holder_to = EphemeralKeyHolder::new(&to_npk); - let shared_secret_to = eph_holder_to.calculate_shared_secret_sender(&to_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[1, 1], - &produce_random_nonces(2), - &[ - (from_npk.clone(), shared_secret_from.clone()), - (to_npk.clone(), shared_secret_to.clone()), - ], - &[ - (from_nsk.unwrap(), from_proof.unwrap()), - (to_nsk.unwrap(), to_proof), - ], - &program, - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - ( - from_npk.clone(), - from_ipk.clone(), - eph_holder_from.generate_ephemeral_public_key(), - ), - ( - to_npk.clone(), - to_ipk.clone(), - eph_holder_to.generate_ephemeral_public_key(), - ), - ], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret_from, shared_secret_to], - )) - } - - pub(crate) async fn private_tx_two_accs_receiver_uninit( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: from_nsk, - npk: from_npk, - ipk: from_ipk, - auth_acc: sender_pre, - proof: from_proof, - } = self.private_acc_preparation(from, true, true).await?; - - let AccountPreparedData { - nsk: _, - npk: to_npk, - ipk: to_ipk, - auth_acc: recipient_pre, - proof: _, - } = self.private_acc_preparation(to, false, false).await?; - - tx_pre_check(&sender_pre.account, &recipient_pre.account)?; - - let eph_holder_from = EphemeralKeyHolder::new(&from_npk); - let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_ipk); - - let eph_holder_to = EphemeralKeyHolder::new(&to_npk); - let shared_secret_to = eph_holder_to.calculate_shared_secret_sender(&to_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[1, 2], - &produce_random_nonces(2), - &[ - (from_npk.clone(), shared_secret_from.clone()), - (to_npk.clone(), shared_secret_to.clone()), - ], - &[(from_nsk.unwrap(), from_proof.unwrap())], - &program, - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - ( - from_npk.clone(), - from_ipk.clone(), - eph_holder_from.generate_ephemeral_public_key(), - ), - ( - to_npk.clone(), - to_ipk.clone(), - eph_holder_to.generate_ephemeral_public_key(), - ), - ], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret_from, shared_secret_to], - )) - } - - pub(crate) async fn private_tx_two_accs_receiver_outer( - &self, - from: AccountId, - to_npk: NullifierPublicKey, - to_ipk: IncomingViewingPublicKey, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: from_nsk, - npk: from_npk, - ipk: from_ipk, - auth_acc: sender_pre, - proof: from_proof, - } = self.private_acc_preparation(from, true, true).await?; - - let to_acc = nssa_core::account::Account::default(); - - tx_pre_check(&sender_pre.account, &to_acc)?; - - let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, &to_npk); - - let eph_holder = EphemeralKeyHolder::new(&to_npk); - - let shared_secret_from = eph_holder.calculate_shared_secret_sender(&from_ipk); - let shared_secret_to = eph_holder.calculate_shared_secret_sender(&to_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[1, 2], - &produce_random_nonces(2), - &[ - (from_npk.clone(), shared_secret_from.clone()), - (to_npk.clone(), shared_secret_to.clone()), - ], - &[(from_nsk.unwrap(), from_proof.unwrap())], - &program, - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - ( - from_npk.clone(), - from_ipk.clone(), - eph_holder.generate_ephemeral_public_key(), - ), - ( - to_npk.clone(), - to_ipk.clone(), - eph_holder.generate_ephemeral_public_key(), - ), - ], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret_from, shared_secret_to], - )) - } - - pub(crate) async fn deshielded_tx_two_accs( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result<(SendTxResponse, [nssa_core::SharedSecretKey; 1]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: from_nsk, - npk: from_npk, - ipk: from_ipk, - auth_acc: sender_pre, - proof: from_proof, - } = self.private_acc_preparation(from, true, true).await?; - - let Ok(to_acc) = self.get_account_public(to).await else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - tx_pre_check(&sender_pre.account, &to_acc)?; - - let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, to); - - let eph_holder = EphemeralKeyHolder::new(&from_npk); - let shared_secret = eph_holder.calculate_shared_secret_sender(&from_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[1, 0], - &produce_random_nonces(1), - &[(from_npk.clone(), shared_secret.clone())], - &[(from_nsk.unwrap(), from_proof.unwrap())], - &program, - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![to], - vec![], - vec![( - from_npk.clone(), - from_ipk.clone(), - eph_holder.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret], - )) - } - - pub(crate) async fn shielded_two_accs_all_init( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - to_proof: MembershipProof, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { - let Ok(from_acc) = self.get_account_public(from).await else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let AccountPreparedData { - nsk: to_nsk, - npk: to_npk, - ipk: to_ipk, - auth_acc: recipient_pre, - proof: _, - } = self.private_acc_preparation(to, true, false).await?; - - tx_pre_check(&from_acc, &recipient_pre.account)?; - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); - - let eph_holder = EphemeralKeyHolder::new(&to_npk); - let shared_secret = eph_holder.calculate_shared_secret_sender(&to_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[0, 1], - &produce_random_nonces(1), - &[(to_npk.clone(), shared_secret.clone())], - &[(to_nsk.unwrap(), to_proof)], - &program, - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![from], - vec![from_acc.nonce], - vec![( - to_npk.clone(), - to_ipk.clone(), - eph_holder.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); - - let Some(signing_key) = signing_key else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret], - )) - } - - pub(crate) async fn shielded_two_accs_receiver_uninit( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { - let Ok(from_acc) = self.get_account_public(from).await else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let AccountPreparedData { - nsk: _, - npk: to_npk, - ipk: to_ipk, - auth_acc: recipient_pre, - proof: _, - } = self.private_acc_preparation(to, false, false).await?; - - tx_pre_check(&from_acc, &recipient_pre.account)?; - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); - - let eph_holder = EphemeralKeyHolder::new(&to_npk); - let shared_secret = eph_holder.calculate_shared_secret_sender(&to_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[0, 2], - &produce_random_nonces(1), - &[(to_npk.clone(), shared_secret.clone())], - &[], - &program, - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![from], - vec![from_acc.nonce], - vec![( - to_npk.clone(), - to_ipk.clone(), - eph_holder.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); - - let Some(signing_key) = signing_key else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret], - )) - } - - pub(crate) async fn shielded_two_accs_receiver_outer( - &self, - from: AccountId, - to_npk: NullifierPublicKey, - to_ipk: IncomingViewingPublicKey, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result { - let Ok(from_acc) = self.get_account_public(from).await else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let to_acc = Account::default(); - - tx_pre_check(&from_acc, &to_acc)?; - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); - let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, &to_npk); - - let eph_holder = EphemeralKeyHolder::new(&to_npk); - let shared_secret = eph_holder.calculate_shared_secret_sender(&to_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[0, 2], - &produce_random_nonces(1), - &[(to_npk.clone(), shared_secret.clone())], - &[], - &program, - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![from], - vec![from_acc.nonce], - vec![( - to_npk.clone(), - to_ipk.clone(), - eph_holder.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); - - let Some(signing_key) = signing_key else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(self.sequencer_client.send_tx_private(tx).await?) - } - + // TODO: Remove pub async fn register_account_under_authenticated_transfers_programs_private( &self, from: AccountId, From 777486ce2cb39d666f3a9eafc7c8427b49863fe5 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 01:57:59 +0300 Subject: [PATCH 24/70] refactor: implement program interactions as facades --- .../src/cli/programs/native_token_transfer.rs | 29 +- wallet/src/cli/programs/pinata.rs | 28 +- wallet/src/cli/programs/token.rs | 33 +- wallet/src/lib.rs | 11 +- wallet/src/privacy_preserving_tx.rs | 4 +- wallet/src/program_facades/mod.rs | 6 + .../native_token_transfer/deshielded.rs | 35 +++ .../native_token_transfer/mod.rs | 33 ++ .../native_token_transfer/private.rs | 68 ++++ .../native_token_transfer}/public.rs | 22 +- .../native_token_transfer/shielded.rs | 68 ++++ wallet/src/program_facades/pinata.rs | 53 ++++ wallet/src/program_facades/token.rs | 294 ++++++++++++++++++ wallet/src/program_interactions/mod.rs | 2 - wallet/src/program_interactions/pinata.rs | 161 ---------- wallet/src/program_interactions/token.rs | 290 ----------------- wallet/src/token_transfers/deshielded.rs | 34 -- wallet/src/token_transfers/mod.rs | 33 -- wallet/src/token_transfers/private.rs | 67 ---- wallet/src/token_transfers/shielded.rs | 67 ---- 20 files changed, 612 insertions(+), 726 deletions(-) create mode 100644 wallet/src/program_facades/mod.rs create mode 100644 wallet/src/program_facades/native_token_transfer/deshielded.rs create mode 100644 wallet/src/program_facades/native_token_transfer/mod.rs create mode 100644 wallet/src/program_facades/native_token_transfer/private.rs rename wallet/src/{token_transfers => program_facades/native_token_transfer}/public.rs (73%) create mode 100644 wallet/src/program_facades/native_token_transfer/shielded.rs create mode 100644 wallet/src/program_facades/pinata.rs create mode 100644 wallet/src/program_facades/token.rs delete mode 100644 wallet/src/program_interactions/mod.rs delete mode 100644 wallet/src/program_interactions/pinata.rs delete mode 100644 wallet/src/program_interactions/token.rs delete mode 100644 wallet/src/token_transfers/deshielded.rs delete mode 100644 wallet/src/token_transfers/mod.rs delete mode 100644 wallet/src/token_transfers/private.rs delete mode 100644 wallet/src/token_transfers/shielded.rs diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 3b1f2ae..12c263f 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -7,6 +7,7 @@ use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, + program_facades::native_token_transfer::NativeTokenTransfer, }; /// Represents generic CLI subcommand for a wallet working with native token transfer program @@ -56,8 +57,8 @@ impl WalletSubcommand for AuthTransferSubcommand { AccountPrivacyKind::Public => { let account_id = account_id.parse()?; - let res = wallet_core - .register_account_under_authenticated_transfers_programs(account_id) + let res = NativeTokenTransfer(wallet_core) + .register_account(account_id) .await?; println!("Results of tx send is {res:#?}"); @@ -317,8 +318,8 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); - let (res, [secret_from, secret_to]) = wallet_core - .send_private_native_token_transfer_owned_account(from, to, amount) + let (res, [secret_from, secret_to]) = NativeTokenTransfer(wallet_core) + .send_private_transfer_to_owned_account(from, to, amount) .await?; println!("Results of tx send is {res:#?}"); @@ -361,8 +362,8 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { let to_ipk = nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_ipk.to_vec()); - let (res, [secret_from, _]) = wallet_core - .send_private_native_token_transfer_outer_account(from, to_npk, to_ipk, amount) + let (res, [secret_from, _]) = NativeTokenTransfer(wallet_core) + .send_private_transfer_to_outer_account(from, to_npk, to_ipk, amount) .await?; println!("Results of tx send is {res:#?}"); @@ -401,8 +402,8 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); - let (res, secret) = wallet_core - .send_shielded_native_token_transfer(from, to, amount) + let (res, secret) = NativeTokenTransfer(wallet_core) + .send_shielded_transfer(from, to, amount) .await?; println!("Results of tx send is {res:#?}"); @@ -446,8 +447,8 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { let to_ipk = nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_ipk.to_vec()); - let (res, _) = wallet_core - .send_shielded_native_token_transfer_outer_account(from, to_npk, to_ipk, amount) + let (res, _) = NativeTokenTransfer(wallet_core) + .send_shielded_transfer_to_outer_account(from, to_npk, to_ipk, amount) .await?; println!("Results of tx send is {res:#?}"); @@ -480,8 +481,8 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); - let (res, secret) = wallet_core - .send_deshielded_native_token_transfer(from, to, amount) + let (res, secret) = NativeTokenTransfer(wallet_core) + .send_deshielded_transfer(from, to, amount) .await?; println!("Results of tx send is {res:#?}"); @@ -510,8 +511,8 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { let from: AccountId = from.parse().unwrap(); let to: AccountId = to.parse().unwrap(); - let res = wallet_core - .send_public_native_token_transfer(from, to, amount) + let res = NativeTokenTransfer(wallet_core) + .send_public_transfer(from, to, amount) .await?; println!("Results of tx send is {res:#?}"); diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index 3d8dac3..cabee4c 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -7,6 +7,7 @@ use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, + program_facades::pinata::Pinata, }; /// Represents generic CLI subcommand for a wallet working with pinata program @@ -117,8 +118,8 @@ impl WalletSubcommand for PinataProgramSubcommandPublic { winner_account_id, solution, } => { - let res = wallet_core - .claim_pinata( + let res = Pinata(wallet_core) + .claim( pinata_account_id.parse().unwrap(), winner_account_id.parse().unwrap(), solution, @@ -146,29 +147,10 @@ impl WalletSubcommand for PinataProgramSubcommandPrivate { let pinata_account_id = pinata_account_id.parse().unwrap(); let winner_account_id = winner_account_id.parse().unwrap(); - let winner_initialization = wallet_core - .check_private_account_initialized(&winner_account_id) + let (res, secret_winner) = Pinata(wallet_core) + .claim_private_owned_account(pinata_account_id, winner_account_id, solution) .await?; - let (res, [secret_winner]) = if let Some(winner_proof) = winner_initialization { - wallet_core - .claim_pinata_private_owned_account_already_initialized( - pinata_account_id, - winner_account_id, - solution, - winner_proof, - ) - .await? - } else { - wallet_core - .claim_pinata_private_owned_account_not_initialized( - pinata_account_id, - winner_account_id, - solution, - ) - .await? - }; - info!("Results of tx send is {res:#?}"); let tx_hash = res.tx_hash; diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 0671e4b..1fceb74 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -7,6 +7,7 @@ use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, + program_facades::token::Token, }; /// Represents generic CLI subcommand for a wallet working with token program @@ -338,8 +339,8 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { } let mut name_bytes = [0; 6]; name_bytes[..name.len()].copy_from_slice(name); - wallet_core - .send_new_token_definition( + Token(wallet_core) + .send_new_definition( definition_account_id.parse().unwrap(), supply_account_id.parse().unwrap(), name_bytes, @@ -353,8 +354,8 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { recipient_account_id, balance_to_move, } => { - wallet_core - .send_transfer_token_transaction( + Token(wallet_core) + .send_transfer_transaction( sender_account_id.parse().unwrap(), recipient_account_id.parse().unwrap(), balance_to_move, @@ -389,8 +390,8 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { let definition_account_id: AccountId = definition_account_id.parse().unwrap(); let supply_account_id: AccountId = supply_account_id.parse().unwrap(); - let (res, secret_supply) = wallet_core - .send_new_token_definition_private_owned( + let (res, secret_supply) = Token(wallet_core) + .send_new_definition_private_owned( definition_account_id, supply_account_id, name_bytes, @@ -428,8 +429,8 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { let sender_account_id: AccountId = sender_account_id.parse().unwrap(); let recipient_account_id: AccountId = recipient_account_id.parse().unwrap(); - let (res, [secret_sender, secret_recipient]) = wallet_core - .send_transfer_token_transaction_private_owned_account( + let (res, [secret_sender, secret_recipient]) = Token(wallet_core) + .send_transfer_transaction_private_owned_account( sender_account_id, recipient_account_id, balance_to_move, @@ -480,8 +481,8 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { recipient_ipk.to_vec(), ); - let (res, [secret_sender, _]) = wallet_core - .send_transfer_token_transaction_private_foreign_account( + let (res, [secret_sender, _]) = Token(wallet_core) + .send_transfer_transaction_private_foreign_account( sender_account_id, recipient_npk, recipient_ipk, @@ -529,8 +530,8 @@ impl WalletSubcommand for TokenProgramSubcommandDeshielded { let sender_account_id: AccountId = sender_account_id.parse().unwrap(); let recipient_account_id: AccountId = recipient_account_id.parse().unwrap(); - let (res, secret_sender) = wallet_core - .send_transfer_token_transaction_deshielded( + let (res, secret_sender) = Token(wallet_core) + .send_transfer_transaction_deshielded( sender_account_id, recipient_account_id, balance_to_move, @@ -588,8 +589,8 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { recipient_ipk.to_vec(), ); - let (res, _) = wallet_core - .send_transfer_token_transaction_shielded_foreign_account( + let (res, _) = Token(wallet_core) + .send_transfer_transaction_shielded_foreign_account( sender_account_id, recipient_npk, recipient_ipk, @@ -622,8 +623,8 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { let sender_account_id: AccountId = sender_account_id.parse().unwrap(); let recipient_account_id: AccountId = recipient_account_id.parse().unwrap(); - let (res, secret_recipient) = wallet_core - .send_transfer_token_transaction_shielded_owned_account( + let (res, secret_recipient) = Token(wallet_core) + .send_transfer_transaction_shielded_owned_account( sender_account_id, recipient_account_id, balance_to_move, diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 7a009c3..d6800b0 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -31,8 +31,7 @@ pub mod config; pub mod helperfunctions; pub mod poller; mod privacy_preserving_tx; -pub mod program_interactions; -pub mod token_transfers; +pub mod program_facades; pub mod transaction_utils; pub struct WalletCore { @@ -214,9 +213,9 @@ impl WalletCore { pub async fn send_privacy_preserving_tx( &self, accounts: Vec, - instruction_data: InstructionData, + instruction_data: &InstructionData, tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, - program: Program, + program: &Program, ) -> Result<(SendTxResponse, Vec), ExecutionFailureKind> { let payload = privacy_preserving_tx::Payload::new(self, accounts).await?; @@ -231,7 +230,7 @@ impl WalletCore { let private_account_keys = payload.private_account_keys(); let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( &pre_states, - &instruction_data, + instruction_data, payload.visibility_mask(), &produce_random_nonces(private_account_keys.len()), &private_account_keys @@ -239,7 +238,7 @@ impl WalletCore { .map(|keys| (keys.npk.clone(), keys.ssk.clone())) .collect::>(), &payload.private_account_auth(), - &program, + program, ) .unwrap(); diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index be96a7a..2d670c3 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -11,7 +11,7 @@ use crate::{WalletCore, transaction_utils::AccountPreparedData}; pub enum PrivacyPreservingAccount { Public(AccountId), - PrivateLocal(AccountId), + PrivateOwned(AccountId), PrivateForeign { npk: NullifierPublicKey, ipk: IncomingViewingPublicKey, @@ -59,7 +59,7 @@ impl Payload { (State::Public { account, sk }, 0) } - PrivacyPreservingAccount::PrivateLocal(account_id) => { + PrivacyPreservingAccount::PrivateOwned(account_id) => { let mut pre = wallet .private_acc_preparation(account_id, true, true) .await?; diff --git a/wallet/src/program_facades/mod.rs b/wallet/src/program_facades/mod.rs new file mode 100644 index 0000000..27d30ce --- /dev/null +++ b/wallet/src/program_facades/mod.rs @@ -0,0 +1,6 @@ +//! This module contains [`WalletCore`](crate::WalletCore) facades for interacting with various +//! on-chain programs. + +pub mod native_token_transfer; +pub mod pinata; +pub mod token; diff --git a/wallet/src/program_facades/native_token_transfer/deshielded.rs b/wallet/src/program_facades/native_token_transfer/deshielded.rs new file mode 100644 index 0000000..f4e45b6 --- /dev/null +++ b/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -0,0 +1,35 @@ +use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use nssa::AccountId; + +use super::{NativeTokenTransfer, auth_transfer_preparation}; +use crate::PrivacyPreservingAccount; + +impl NativeTokenTransfer<'_> { + pub async fn send_deshielded_transfer( + &self, + from: AccountId, + to: AccountId, + balance_to_move: u128, + ) -> Result<(SendTxResponse, nssa_core::SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(from), + PrivacyPreservingAccount::Public(to), + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected sender's secret"); + (resp, first) + }) + } +} diff --git a/wallet/src/program_facades/native_token_transfer/mod.rs b/wallet/src/program_facades/native_token_transfer/mod.rs new file mode 100644 index 0000000..693ef8d --- /dev/null +++ b/wallet/src/program_facades/native_token_transfer/mod.rs @@ -0,0 +1,33 @@ +use common::error::ExecutionFailureKind; +use nssa::{Account, program::Program}; +use nssa_core::program::InstructionData; + +use crate::WalletCore; + +pub mod deshielded; +pub mod private; +pub mod public; +pub mod shielded; + +pub struct NativeTokenTransfer<'w>(pub &'w WalletCore); + +fn auth_transfer_preparation( + balance_to_move: u128, +) -> ( + InstructionData, + Program, + impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, +) { + let instruction_data = Program::serialize_instruction(balance_to_move).unwrap(); + let program = Program::authenticated_transfer_program(); + let tx_pre_check = move |accounts: &[&Account]| { + let from = accounts[0]; + if from.balance >= balance_to_move { + Ok(()) + } else { + Err(ExecutionFailureKind::InsufficientFundsError) + } + }; + + (instruction_data, program, tx_pre_check) +} diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs new file mode 100644 index 0000000..39a4781 --- /dev/null +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -0,0 +1,68 @@ +use std::vec; + +use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use nssa::AccountId; +use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; + +use super::{NativeTokenTransfer, auth_transfer_preparation}; +use crate::PrivacyPreservingAccount; + +impl NativeTokenTransfer<'_> { + pub async fn send_private_transfer_to_outer_account( + &self, + from: AccountId, + to_npk: NullifierPublicKey, + to_ipk: IncomingViewingPublicKey, + balance_to_move: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(from), + PrivacyPreservingAccount::PrivateForeign { + npk: to_npk, + ipk: to_ipk, + }, + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut secrets_iter = secrets.into_iter(); + let first = secrets_iter.next().expect("expected sender's secret"); + let second = secrets_iter.next().expect("expected receiver's secret"); + (resp, [first, second]) + }) + } + + pub async fn send_private_transfer_to_owned_account( + &self, + from: AccountId, + to: AccountId, + balance_to_move: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(from), + PrivacyPreservingAccount::PrivateOwned(to), + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut secrets_iter = secrets.into_iter(); + let first = secrets_iter.next().expect("expected sender's secret"); + let second = secrets_iter.next().expect("expected receiver's secret"); + (resp, [first, second]) + }) + } +} diff --git a/wallet/src/token_transfers/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs similarity index 73% rename from wallet/src/token_transfers/public.rs rename to wallet/src/program_facades/native_token_transfer/public.rs index a63d838..2edab15 100644 --- a/wallet/src/token_transfers/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -5,21 +5,21 @@ use nssa::{ public_transaction::{Message, WitnessSet}, }; -use crate::WalletCore; +use super::NativeTokenTransfer; -impl WalletCore { - pub async fn send_public_native_token_transfer( +impl NativeTokenTransfer<'_> { + pub async fn send_public_transfer( &self, from: AccountId, to: AccountId, balance_to_move: u128, ) -> Result { - let Ok(balance) = self.get_account_balance(from).await else { + let Ok(balance) = self.0.get_account_balance(from).await else { return Err(ExecutionFailureKind::SequencerError); }; if balance >= balance_to_move { - let Ok(nonces) = self.get_accounts_nonces(vec![from]).await else { + let Ok(nonces) = self.0.get_accounts_nonces(vec![from]).await else { return Err(ExecutionFailureKind::SequencerError); }; @@ -28,7 +28,7 @@ impl WalletCore { let message = Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap(); - let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); + let signing_key = self.0.storage.user_data.get_pub_account_signing_key(&from); let Some(signing_key) = signing_key else { return Err(ExecutionFailureKind::KeyNotFoundError); @@ -38,17 +38,17 @@ impl WalletCore { let tx = PublicTransaction::new(message, witness_set); - Ok(self.sequencer_client.send_tx_public(tx).await?) + Ok(self.0.sequencer_client.send_tx_public(tx).await?) } else { Err(ExecutionFailureKind::InsufficientFundsError) } } - pub async fn register_account_under_authenticated_transfers_programs( + pub async fn register_account( &self, from: AccountId, ) -> Result { - let Ok(nonces) = self.get_accounts_nonces(vec![from]).await else { + let Ok(nonces) = self.0.get_accounts_nonces(vec![from]).await else { return Err(ExecutionFailureKind::SequencerError); }; @@ -57,7 +57,7 @@ impl WalletCore { let program_id = Program::authenticated_transfer_program().id(); let message = Message::try_new(program_id, account_ids, nonces, instruction).unwrap(); - let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); + let signing_key = self.0.storage.user_data.get_pub_account_signing_key(&from); let Some(signing_key) = signing_key else { return Err(ExecutionFailureKind::KeyNotFoundError); @@ -67,6 +67,6 @@ impl WalletCore { let tx = PublicTransaction::new(message, witness_set); - Ok(self.sequencer_client.send_tx_public(tx).await?) + Ok(self.0.sequencer_client.send_tx_public(tx).await?) } } diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs new file mode 100644 index 0000000..d40d5d4 --- /dev/null +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -0,0 +1,68 @@ +use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use nssa::AccountId; +use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; + +use super::{NativeTokenTransfer, auth_transfer_preparation}; +use crate::PrivacyPreservingAccount; + +impl NativeTokenTransfer<'_> { + pub async fn send_shielded_transfer( + &self, + from: AccountId, + to: AccountId, + balance_to_move: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(from), + PrivacyPreservingAccount::PrivateOwned(to), + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected sender's secret"); + (resp, first) + }) + } + + pub async fn send_shielded_transfer_to_outer_account( + &self, + from: AccountId, + to_npk: NullifierPublicKey, + to_ipk: IncomingViewingPublicKey, + balance_to_move: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(from), + PrivacyPreservingAccount::PrivateForeign { + npk: to_npk, + ipk: to_ipk, + }, + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected sender's secret"); + (resp, first) + }) + } +} diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs new file mode 100644 index 0000000..6367bfc --- /dev/null +++ b/wallet/src/program_facades/pinata.rs @@ -0,0 +1,53 @@ +use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use nssa::AccountId; +use nssa_core::SharedSecretKey; + +use crate::{PrivacyPreservingAccount, WalletCore}; + +pub struct Pinata<'w>(pub &'w WalletCore); + +impl Pinata<'_> { + pub async fn claim( + &self, + pinata_account_id: AccountId, + winner_account_id: AccountId, + solution: u128, + ) -> Result { + let account_ids = vec![pinata_account_id, winner_account_id]; + let program_id = nssa::program::Program::pinata().id(); + let message = + nssa::public_transaction::Message::try_new(program_id, account_ids, vec![], solution) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.0.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn claim_private_owned_account( + &self, + pinata_account_id: AccountId, + winner_account_id: AccountId, + solution: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(pinata_account_id), + PrivacyPreservingAccount::PrivateOwned(winner_account_id), + ], + &nssa::program::Program::serialize_instruction(solution).unwrap(), + |_| Ok(()), + &nssa::program::Program::pinata(), + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected recipient's secret"); + (resp, first) + }) + } +} diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs new file mode 100644 index 0000000..a4969de --- /dev/null +++ b/wallet/src/program_facades/token.rs @@ -0,0 +1,294 @@ +use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use nssa::{Account, AccountId, program::Program}; +use nssa_core::{ + NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey, + program::InstructionData, +}; + +use crate::{PrivacyPreservingAccount, WalletCore}; + +pub struct Token<'w>(pub &'w WalletCore); + +impl Token<'_> { + pub async fn send_new_definition( + &self, + definition_account_id: AccountId, + supply_account_id: AccountId, + name: [u8; 6], + total_supply: u128, + ) -> Result { + let account_ids = vec![definition_account_id, supply_account_id]; + 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, + account_ids, + vec![], + instruction, + ) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.0.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn send_new_definition_private_owned( + &self, + definition_account_id: AccountId, + supply_account_id: AccountId, + name: [u8; 6], + total_supply: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = + token_program_preparation_definition(name, total_supply); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(definition_account_id), + PrivacyPreservingAccount::PrivateOwned(supply_account_id), + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected recipient's secret"); + (resp, first) + }) + } + + pub async fn send_transfer_transaction( + &self, + sender_account_id: AccountId, + recipient_account_id: AccountId, + amount: u128, + ) -> Result { + let account_ids = vec![sender_account_id, recipient_account_id]; + 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.0.get_accounts_nonces(vec![sender_account_id]).await else { + return Err(ExecutionFailureKind::SequencerError); + }; + let message = nssa::public_transaction::Message::try_new( + program_id, + account_ids, + nonces, + instruction, + ) + .unwrap(); + + let Some(signing_key) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&sender_account_id) + 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.0.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn send_transfer_transaction_private_owned_account( + &self, + sender_account_id: AccountId, + recipient_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(sender_account_id), + PrivacyPreservingAccount::PrivateOwned(recipient_account_id), + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected sender's secret"); + let second = iter.next().expect("expected recipient's secret"); + (resp, [first, second]) + }) + } + + pub async fn send_transfer_transaction_private_foreign_account( + &self, + sender_account_id: AccountId, + recipient_npk: NullifierPublicKey, + recipient_ipk: IncomingViewingPublicKey, + amount: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(sender_account_id), + PrivacyPreservingAccount::PrivateForeign { + npk: recipient_npk, + ipk: recipient_ipk, + }, + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected sender's secret"); + let second = iter.next().expect("expected recipient's secret"); + (resp, [first, second]) + }) + } + + pub async fn send_transfer_transaction_deshielded( + &self, + sender_account_id: AccountId, + recipient_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(sender_account_id), + PrivacyPreservingAccount::Public(recipient_account_id), + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected sender's secret"); + (resp, first) + }) + } + + pub async fn send_transfer_transaction_shielded_owned_account( + &self, + sender_account_id: AccountId, + recipient_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(sender_account_id), + PrivacyPreservingAccount::PrivateOwned(recipient_account_id), + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected recipient's secret"); + (resp, first) + }) + } + + pub async fn send_transfer_transaction_shielded_foreign_account( + &self, + sender_account_id: AccountId, + recipient_npk: NullifierPublicKey, + recipient_ipk: IncomingViewingPublicKey, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(sender_account_id), + PrivacyPreservingAccount::PrivateForeign { + npk: recipient_npk, + ipk: recipient_ipk, + }, + ], + &instruction_data, + tx_pre_check, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected recipient's secret"); + (resp, first) + }) + } +} + +fn token_program_preparation_transfer( + amount: u128, +) -> ( + InstructionData, + Program, + impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, +) { + // 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 instruction_data = Program::serialize_instruction(instruction).unwrap(); + let program = Program::token(); + let tx_pre_check = |_: &[&Account]| Ok(()); + + (instruction_data, program, tx_pre_check) +} + +fn token_program_preparation_definition( + name: [u8; 6], + total_supply: u128, +) -> ( + InstructionData, + Program, + impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, +) { + // 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 instruction_data = Program::serialize_instruction(instruction).unwrap(); + let program = Program::token(); + let tx_pre_check = |_: &[&Account]| Ok(()); + + (instruction_data, program, tx_pre_check) +} diff --git a/wallet/src/program_interactions/mod.rs b/wallet/src/program_interactions/mod.rs deleted file mode 100644 index fbdd6ab..0000000 --- a/wallet/src/program_interactions/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod pinata; -pub mod token; diff --git a/wallet/src/program_interactions/pinata.rs b/wallet/src/program_interactions/pinata.rs deleted file mode 100644 index e5150c5..0000000 --- a/wallet/src/program_interactions/pinata.rs +++ /dev/null @@ -1,161 +0,0 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; -use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; -use nssa::{AccountId, privacy_preserving_transaction::circuit}; -use nssa_core::{MembershipProof, SharedSecretKey, account::AccountWithMetadata}; - -use crate::{ - WalletCore, helperfunctions::produce_random_nonces, transaction_utils::AccountPreparedData, -}; - -impl WalletCore { - pub async fn claim_pinata( - &self, - pinata_account_id: AccountId, - winner_account_id: AccountId, - solution: u128, - ) -> Result { - let account_ids = vec![pinata_account_id, winner_account_id]; - let program_id = nssa::program::Program::pinata().id(); - let message = - nssa::public_transaction::Message::try_new(program_id, account_ids, vec![], solution) - .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_public(tx).await?) - } - - pub async fn claim_pinata_private_owned_account_already_initialized( - &self, - pinata_account_id: AccountId, - winner_account_id: AccountId, - solution: u128, - winner_proof: MembershipProof, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: winner_nsk, - npk: winner_npk, - ipk: winner_ipk, - auth_acc: winner_pre, - proof: _, - } = self - .private_acc_preparation(winner_account_id, true, false) - .await?; - - let pinata_acc = self.get_account_public(pinata_account_id).await.unwrap(); - - let program = nssa::program::Program::pinata(); - - let pinata_pre = AccountWithMetadata::new(pinata_acc.clone(), false, pinata_account_id); - - let eph_holder_winner = EphemeralKeyHolder::new(&winner_npk); - let shared_secret_winner = eph_holder_winner.calculate_shared_secret_sender(&winner_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[pinata_pre, winner_pre], - &nssa::program::Program::serialize_instruction(solution).unwrap(), - &[0, 1], - &produce_random_nonces(1), - &[(winner_npk.clone(), shared_secret_winner.clone())], - &[(winner_nsk.unwrap(), winner_proof)], - &program, - ) - .unwrap(); - - let message = - nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( - vec![pinata_account_id], - vec![], - vec![( - winner_npk.clone(), - winner_ipk.clone(), - eph_holder_winner.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let witness_set = - nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( - &message, - proof, - &[], - ); - let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new( - message, - witness_set, - ); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret_winner], - )) - } - - pub async fn claim_pinata_private_owned_account_not_initialized( - &self, - pinata_account_id: AccountId, - winner_account_id: AccountId, - solution: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: _, - npk: winner_npk, - ipk: winner_ipk, - auth_acc: winner_pre, - proof: _, - } = self - .private_acc_preparation(winner_account_id, false, false) - .await?; - - let pinata_acc = self.get_account_public(pinata_account_id).await.unwrap(); - - let program = nssa::program::Program::pinata(); - - let pinata_pre = AccountWithMetadata::new(pinata_acc.clone(), false, pinata_account_id); - - let eph_holder_winner = EphemeralKeyHolder::new(&winner_npk); - let shared_secret_winner = eph_holder_winner.calculate_shared_secret_sender(&winner_ipk); - - let (output, proof) = circuit::execute_and_prove( - &[pinata_pre, winner_pre], - &nssa::program::Program::serialize_instruction(solution).unwrap(), - &[0, 2], - &produce_random_nonces(1), - &[(winner_npk.clone(), shared_secret_winner.clone())], - &[], - &program, - ) - .unwrap(); - - let message = - nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( - vec![pinata_account_id], - vec![], - vec![( - winner_npk.clone(), - winner_ipk.clone(), - eph_holder_winner.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let witness_set = - nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( - &message, - proof, - &[], - ); - let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new( - message, - witness_set, - ); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret_winner], - )) - } -} diff --git a/wallet/src/program_interactions/token.rs b/wallet/src/program_interactions/token.rs deleted file mode 100644 index 91f76d4..0000000 --- a/wallet/src/program_interactions/token.rs +++ /dev/null @@ -1,290 +0,0 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; -use nssa::{Account, AccountId, program::Program}; -use nssa_core::{ - NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey, - program::InstructionData, -}; - -use crate::{PrivacyPreservingAccount, WalletCore}; - -impl WalletCore { - pub fn token_program_preparation_transfer( - amount: u128, - ) -> ( - InstructionData, - Program, - impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, - ) { - // 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 instruction_data = Program::serialize_instruction(instruction).unwrap(); - let program = Program::token(); - let tx_pre_check = |_: &[&Account]| Ok(()); - - (instruction_data, program, tx_pre_check) - } - - pub fn token_program_preparation_definition( - name: [u8; 6], - total_supply: u128, - ) -> ( - InstructionData, - Program, - impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, - ) { - // 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 instruction_data = Program::serialize_instruction(instruction).unwrap(); - let program = Program::token(); - let tx_pre_check = |_: &[&Account]| Ok(()); - - (instruction_data, program, tx_pre_check) - } - - pub async fn send_new_token_definition( - &self, - definition_account_id: AccountId, - supply_account_id: AccountId, - name: [u8; 6], - total_supply: u128, - ) -> Result { - let account_ids = vec![definition_account_id, supply_account_id]; - 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, - account_ids, - 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_public(tx).await?) - } - - pub async fn send_new_token_definition_private_owned( - &self, - definition_account_id: AccountId, - supply_account_id: AccountId, - name: [u8; 6], - total_supply: u128, - ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::token_program_preparation_definition(name, total_supply); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::Public(definition_account_id), - PrivacyPreservingAccount::PrivateLocal(supply_account_id), - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let first = secrets - .into_iter() - .next() - .expect("expected recipient's secret"); - (resp, first) - }) - } - - pub async fn send_transfer_token_transaction( - &self, - sender_account_id: AccountId, - recipient_account_id: AccountId, - amount: u128, - ) -> Result { - let account_ids = vec![sender_account_id, recipient_account_id]; - 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_account_id]).await else { - return Err(ExecutionFailureKind::SequencerError); - }; - let message = nssa::public_transaction::Message::try_new( - program_id, - account_ids, - nonces, - instruction, - ) - .unwrap(); - - let Some(signing_key) = self - .storage - .user_data - .get_pub_account_signing_key(&sender_account_id) - 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_public(tx).await?) - } - - pub async fn send_transfer_token_transaction_private_owned_account( - &self, - sender_account_id: AccountId, - recipient_account_id: AccountId, - amount: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::token_program_preparation_transfer(amount); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::PrivateLocal(sender_account_id), - PrivacyPreservingAccount::PrivateLocal(recipient_account_id), - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let mut iter = secrets.into_iter(); - let first = iter.next().expect("expected sender's secret"); - let second = iter.next().expect("expected recipient's secret"); - (resp, [first, second]) - }) - } - - pub async fn send_transfer_token_transaction_private_foreign_account( - &self, - sender_account_id: AccountId, - recipient_npk: NullifierPublicKey, - recipient_ipk: IncomingViewingPublicKey, - amount: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::token_program_preparation_transfer(amount); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::PrivateLocal(sender_account_id), - PrivacyPreservingAccount::PrivateForeign { - npk: recipient_npk, - ipk: recipient_ipk, - }, - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let mut iter = secrets.into_iter(); - let first = iter.next().expect("expected sender's secret"); - let second = iter.next().expect("expected recipient's secret"); - (resp, [first, second]) - }) - } - - pub async fn send_transfer_token_transaction_deshielded( - &self, - sender_account_id: AccountId, - recipient_account_id: AccountId, - amount: u128, - ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::token_program_preparation_transfer(amount); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::PrivateLocal(sender_account_id), - PrivacyPreservingAccount::Public(recipient_account_id), - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let first = secrets - .into_iter() - .next() - .expect("expected sender's secret"); - (resp, first) - }) - } - - pub async fn send_transfer_token_transaction_shielded_owned_account( - &self, - sender_account_id: AccountId, - recipient_account_id: AccountId, - amount: u128, - ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::token_program_preparation_transfer(amount); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::Public(sender_account_id), - PrivacyPreservingAccount::PrivateLocal(recipient_account_id), - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let first = secrets - .into_iter() - .next() - .expect("expected recipient's secret"); - (resp, first) - }) - } - - pub async fn send_transfer_token_transaction_shielded_foreign_account( - &self, - sender_account_id: AccountId, - recipient_npk: NullifierPublicKey, - recipient_ipk: IncomingViewingPublicKey, - amount: u128, - ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::token_program_preparation_transfer(amount); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::Public(sender_account_id), - PrivacyPreservingAccount::PrivateForeign { - npk: recipient_npk, - ipk: recipient_ipk, - }, - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let first = secrets - .into_iter() - .next() - .expect("expected recipient's secret"); - (resp, first) - }) - } -} diff --git a/wallet/src/token_transfers/deshielded.rs b/wallet/src/token_transfers/deshielded.rs deleted file mode 100644 index 216bfb5..0000000 --- a/wallet/src/token_transfers/deshielded.rs +++ /dev/null @@ -1,34 +0,0 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; -use nssa::AccountId; - -use crate::{PrivacyPreservingAccount, WalletCore}; - -impl WalletCore { - pub async fn send_deshielded_native_token_transfer( - &self, - from: AccountId, - to: AccountId, - balance_to_move: u128, - ) -> Result<(SendTxResponse, nssa_core::SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::auth_transfer_preparation(balance_to_move); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::PrivateLocal(from), - PrivacyPreservingAccount::Public(to), - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let first = secrets - .into_iter() - .next() - .expect("expected sender's secret"); - (resp, first) - }) - } -} diff --git a/wallet/src/token_transfers/mod.rs b/wallet/src/token_transfers/mod.rs deleted file mode 100644 index 6b09698..0000000 --- a/wallet/src/token_transfers/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -use common::error::ExecutionFailureKind; -use nssa::{Account, program::Program}; -use nssa_core::program::InstructionData; - -use crate::WalletCore; - -pub mod deshielded; -pub mod private; -pub mod public; -pub mod shielded; - -impl WalletCore { - pub fn auth_transfer_preparation( - balance_to_move: u128, - ) -> ( - InstructionData, - Program, - impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, - ) { - let instruction_data = Program::serialize_instruction(balance_to_move).unwrap(); - let program = Program::authenticated_transfer_program(); - let tx_pre_check = move |accounts: &[&Account]| { - let from = accounts[0]; - if from.balance >= balance_to_move { - Ok(()) - } else { - Err(ExecutionFailureKind::InsufficientFundsError) - } - }; - - (instruction_data, program, tx_pre_check) - } -} diff --git a/wallet/src/token_transfers/private.rs b/wallet/src/token_transfers/private.rs deleted file mode 100644 index 59af480..0000000 --- a/wallet/src/token_transfers/private.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::vec; - -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; -use nssa::AccountId; -use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; - -use crate::{PrivacyPreservingAccount, WalletCore}; - -impl WalletCore { - pub async fn send_private_native_token_transfer_outer_account( - &self, - from: AccountId, - to_npk: NullifierPublicKey, - to_ipk: IncomingViewingPublicKey, - balance_to_move: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::auth_transfer_preparation(balance_to_move); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::PrivateLocal(from), - PrivacyPreservingAccount::PrivateForeign { - npk: to_npk, - ipk: to_ipk, - }, - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let mut secrets_iter = secrets.into_iter(); - let first = secrets_iter.next().expect("expected sender's secret"); - let second = secrets_iter.next().expect("expected receiver's secret"); - (resp, [first, second]) - }) - } - - pub async fn send_private_native_token_transfer_owned_account( - &self, - from: AccountId, - to: AccountId, - balance_to_move: u128, - ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::auth_transfer_preparation(balance_to_move); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::PrivateLocal(from), - PrivacyPreservingAccount::PrivateLocal(to), - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let mut secrets_iter = secrets.into_iter(); - let first = secrets_iter.next().expect("expected sender's secret"); - let second = secrets_iter.next().expect("expected receiver's secret"); - (resp, [first, second]) - }) - } -} diff --git a/wallet/src/token_transfers/shielded.rs b/wallet/src/token_transfers/shielded.rs deleted file mode 100644 index a8d28ee..0000000 --- a/wallet/src/token_transfers/shielded.rs +++ /dev/null @@ -1,67 +0,0 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; -use nssa::AccountId; -use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; - -use crate::{PrivacyPreservingAccount, WalletCore}; - -impl WalletCore { - pub async fn send_shielded_native_token_transfer( - &self, - from: AccountId, - to: AccountId, - balance_to_move: u128, - ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::auth_transfer_preparation(balance_to_move); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::Public(from), - PrivacyPreservingAccount::PrivateLocal(to), - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let first = secrets - .into_iter() - .next() - .expect("expected sender's secret"); - (resp, first) - }) - } - - pub async fn send_shielded_native_token_transfer_outer_account( - &self, - from: AccountId, - to_npk: NullifierPublicKey, - to_ipk: IncomingViewingPublicKey, - balance_to_move: u128, - ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - WalletCore::auth_transfer_preparation(balance_to_move); - - self.send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::Public(from), - PrivacyPreservingAccount::PrivateForeign { - npk: to_npk, - ipk: to_ipk, - }, - ], - instruction_data, - tx_pre_check, - program, - ) - .await - .map(|(resp, secrets)| { - let first = secrets - .into_iter() - .next() - .expect("expected sender's secret"); - (resp, first) - }) - } -} From a8abec196bfd4d151dc1edf734dce2d091f69597 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 02:18:38 +0300 Subject: [PATCH 25/70] refactor: small adjustments to privacy preserving tx sending --- .../src/cli/programs/native_token_transfer.rs | 6 +- wallet/src/lib.rs | 31 +++-- wallet/src/privacy_preserving_tx.rs | 67 +++++++--- .../native_token_transfer/deshielded.rs | 4 +- .../native_token_transfer/private.rs | 31 ++++- .../native_token_transfer/shielded.rs | 8 +- wallet/src/program_facades/pinata.rs | 1 - wallet/src/program_facades/token.rs | 41 ++---- wallet/src/transaction_utils.rs | 117 ------------------ 9 files changed, 119 insertions(+), 187 deletions(-) delete mode 100644 wallet/src/transaction_utils.rs diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 12c263f..00940b3 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -75,10 +75,8 @@ impl WalletSubcommand for AuthTransferSubcommand { AccountPrivacyKind::Private => { let account_id = account_id.parse()?; - let (res, [secret]) = wallet_core - .register_account_under_authenticated_transfers_programs_private( - account_id, - ) + let (res, secret) = NativeTokenTransfer(wallet_core) + .register_account_private(account_id) .await?; println!("Results of tx send is {res:#?}"); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index d6800b0..3b4e38c 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -32,7 +32,6 @@ pub mod helperfunctions; pub mod poller; mod privacy_preserving_tx; pub mod program_facades; -pub mod transaction_utils; pub struct WalletCore { pub storage: WalletChainStore, @@ -214,12 +213,24 @@ impl WalletCore { &self, accounts: Vec, instruction_data: &InstructionData, - tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, program: &Program, ) -> Result<(SendTxResponse, Vec), ExecutionFailureKind> { - let payload = privacy_preserving_tx::Payload::new(self, accounts).await?; + self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| { + Ok(()) + }) + .await + } - let pre_states = payload.pre_states(); + pub async fn send_privacy_preserving_tx_with_pre_check( + &self, + accounts: Vec, + instruction_data: &InstructionData, + program: &Program, + tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, + ) -> Result<(SendTxResponse, Vec), ExecutionFailureKind> { + let acc_manager = privacy_preserving_tx::AccountManager::new(self, accounts).await?; + + let pre_states = acc_manager.pre_states(); tx_pre_check( &pre_states .iter() @@ -227,25 +238,25 @@ impl WalletCore { .collect::>(), )?; - let private_account_keys = payload.private_account_keys(); + let private_account_keys = acc_manager.private_account_keys(); let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( &pre_states, instruction_data, - payload.visibility_mask(), + acc_manager.visibility_mask(), &produce_random_nonces(private_account_keys.len()), &private_account_keys .iter() .map(|keys| (keys.npk.clone(), keys.ssk.clone())) .collect::>(), - &payload.private_account_auth(), + &acc_manager.private_account_auth(), program, ) .unwrap(); let message = nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( - payload.public_account_ids(), - Vec::from_iter(payload.public_account_nonces()), + acc_manager.public_account_ids(), + Vec::from_iter(acc_manager.public_account_nonces()), private_account_keys .iter() .map(|keys| (keys.npk.clone(), keys.ipk.clone(), keys.epk.clone())) @@ -258,7 +269,7 @@ impl WalletCore { nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( &message, proof, - &payload.witness_signing_keys(), + &acc_manager.witness_signing_keys(), ); let tx = PrivacyPreservingTransaction::new(message, witness_set); diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 2d670c3..e8e14d9 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -7,7 +7,7 @@ use nssa_core::{ encryption::{EphemeralPublicKey, IncomingViewingPublicKey}, }; -use crate::{WalletCore, transaction_utils::AccountPreparedData}; +use crate::WalletCore; pub enum PrivacyPreservingAccount { Public(AccountId), @@ -33,12 +33,12 @@ enum State { Private(AccountPreparedData), } -pub struct Payload { +pub struct AccountManager { states: Vec, visibility_mask: Vec, } -impl Payload { +impl AccountManager { pub async fn new( wallet: &WalletCore, accounts: Vec, @@ -60,16 +60,8 @@ impl Payload { (State::Public { account, sk }, 0) } PrivacyPreservingAccount::PrivateOwned(account_id) => { - let mut pre = wallet - .private_acc_preparation(account_id, true, true) - .await?; - let mut mask = 1; - - if pre.proof.is_none() { - pre.auth_acc.is_authorized = false; - pre.nsk = None; - mask = 2 - }; + let pre = private_acc_preparation(wallet, account_id).await?; + let mask = if pre.auth_acc.is_authorized { 1 } else { 2 }; (State::Private(pre), mask) } @@ -116,7 +108,7 @@ impl Payload { self.states .iter() .filter_map(|state| match state { - State::Public { account, .. } => Some(account.account.nonce), + State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce), _ => None, }) .collect() @@ -171,3 +163,50 @@ impl Payload { .collect() } } + +struct AccountPreparedData { + nsk: Option, + npk: NullifierPublicKey, + ipk: IncomingViewingPublicKey, + auth_acc: AccountWithMetadata, + proof: Option, +} + +async fn private_acc_preparation( + wallet: &WalletCore, + account_id: AccountId, +) -> Result { + let Some((from_keys, from_acc)) = wallet + .storage + .user_data + .get_private_account(&account_id) + .cloned() + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let mut nsk = Some(from_keys.private_key_holder.nullifier_secret_key); + + let from_npk = from_keys.nullifer_public_key; + let from_ipk = from_keys.incoming_viewing_public_key; + + // TODO: Remove this unwrap, error types must be compatible + let proof = wallet + .check_private_account_initialized(&account_id) + .await + .unwrap(); + + if proof.is_none() { + nsk = None; + } + + let sender_pre = AccountWithMetadata::new(from_acc.clone(), proof.is_some(), &from_npk); + + Ok(AccountPreparedData { + nsk, + npk: from_npk, + ipk: from_ipk, + auth_acc: sender_pre, + proof, + }) +} diff --git a/wallet/src/program_facades/native_token_transfer/deshielded.rs b/wallet/src/program_facades/native_token_transfer/deshielded.rs index f4e45b6..a25be2c 100644 --- a/wallet/src/program_facades/native_token_transfer/deshielded.rs +++ b/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -14,14 +14,14 @@ impl NativeTokenTransfer<'_> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); self.0 - .send_privacy_preserving_tx( + .send_privacy_preserving_tx_with_pre_check( vec![ PrivacyPreservingAccount::PrivateOwned(from), PrivacyPreservingAccount::Public(to), ], &instruction_data, - tx_pre_check, &program, + tx_pre_check, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs index 39a4781..fcf6eee 100644 --- a/wallet/src/program_facades/native_token_transfer/private.rs +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -1,13 +1,34 @@ use std::vec; use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; -use nssa::AccountId; +use nssa::{AccountId, program::Program}; use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; use super::{NativeTokenTransfer, auth_transfer_preparation}; use crate::PrivacyPreservingAccount; impl NativeTokenTransfer<'_> { + pub async fn register_account_private( + &self, + from: AccountId, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let instruction: u128 = 0; + + self.0 + .send_privacy_preserving_tx_with_pre_check( + vec![PrivacyPreservingAccount::PrivateOwned(from)], + &Program::serialize_instruction(instruction).unwrap(), + &Program::authenticated_transfer_program(), + |_| Ok(()), + ) + .await + .map(|(resp, secrets)| { + let mut secrets_iter = secrets.into_iter(); + let first = secrets_iter.next().expect("expected sender's secret"); + (resp, first) + }) + } + pub async fn send_private_transfer_to_outer_account( &self, from: AccountId, @@ -18,7 +39,7 @@ impl NativeTokenTransfer<'_> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); self.0 - .send_privacy_preserving_tx( + .send_privacy_preserving_tx_with_pre_check( vec![ PrivacyPreservingAccount::PrivateOwned(from), PrivacyPreservingAccount::PrivateForeign { @@ -27,8 +48,8 @@ impl NativeTokenTransfer<'_> { }, ], &instruction_data, - tx_pre_check, &program, + tx_pre_check, ) .await .map(|(resp, secrets)| { @@ -48,14 +69,14 @@ impl NativeTokenTransfer<'_> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); self.0 - .send_privacy_preserving_tx( + .send_privacy_preserving_tx_with_pre_check( vec![ PrivacyPreservingAccount::PrivateOwned(from), PrivacyPreservingAccount::PrivateOwned(to), ], &instruction_data, - tx_pre_check, &program, + tx_pre_check, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index d40d5d4..c049b13 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -15,14 +15,14 @@ impl NativeTokenTransfer<'_> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); self.0 - .send_privacy_preserving_tx( + .send_privacy_preserving_tx_with_pre_check( vec![ PrivacyPreservingAccount::Public(from), PrivacyPreservingAccount::PrivateOwned(to), ], &instruction_data, - tx_pre_check, &program, + tx_pre_check, ) .await .map(|(resp, secrets)| { @@ -44,7 +44,7 @@ impl NativeTokenTransfer<'_> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); self.0 - .send_privacy_preserving_tx( + .send_privacy_preserving_tx_with_pre_check( vec![ PrivacyPreservingAccount::Public(from), PrivacyPreservingAccount::PrivateForeign { @@ -53,8 +53,8 @@ impl NativeTokenTransfer<'_> { }, ], &instruction_data, - tx_pre_check, &program, + tx_pre_check, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs index 6367bfc..46bc7a1 100644 --- a/wallet/src/program_facades/pinata.rs +++ b/wallet/src/program_facades/pinata.rs @@ -38,7 +38,6 @@ impl Pinata<'_> { PrivacyPreservingAccount::PrivateOwned(winner_account_id), ], &nssa::program::Program::serialize_instruction(solution).unwrap(), - |_| Ok(()), &nssa::program::Program::pinata(), ) .await diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index a4969de..298c4f4 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -1,5 +1,5 @@ use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; -use nssa::{Account, AccountId, program::Program}; +use nssa::{AccountId, program::Program}; use nssa_core::{ NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey, program::InstructionData, @@ -45,8 +45,7 @@ impl Token<'_> { name: [u8; 6], total_supply: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = - token_program_preparation_definition(name, total_supply); + let (instruction_data, program) = token_program_preparation_definition(name, total_supply); self.0 .send_privacy_preserving_tx( @@ -55,7 +54,6 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(supply_account_id), ], &instruction_data, - tx_pre_check, &program, ) .await @@ -114,7 +112,7 @@ impl Token<'_> { recipient_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + let (instruction_data, program) = token_program_preparation_transfer(amount); self.0 .send_privacy_preserving_tx( @@ -123,7 +121,6 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(recipient_account_id), ], &instruction_data, - tx_pre_check, &program, ) .await @@ -142,7 +139,7 @@ impl Token<'_> { recipient_ipk: IncomingViewingPublicKey, amount: u128, ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + let (instruction_data, program) = token_program_preparation_transfer(amount); self.0 .send_privacy_preserving_tx( @@ -154,7 +151,6 @@ impl Token<'_> { }, ], &instruction_data, - tx_pre_check, &program, ) .await @@ -172,7 +168,7 @@ impl Token<'_> { recipient_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + let (instruction_data, program) = token_program_preparation_transfer(amount); self.0 .send_privacy_preserving_tx( @@ -181,7 +177,6 @@ impl Token<'_> { PrivacyPreservingAccount::Public(recipient_account_id), ], &instruction_data, - tx_pre_check, &program, ) .await @@ -200,7 +195,7 @@ impl Token<'_> { recipient_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + let (instruction_data, program) = token_program_preparation_transfer(amount); self.0 .send_privacy_preserving_tx( @@ -209,7 +204,6 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(recipient_account_id), ], &instruction_data, - tx_pre_check, &program, ) .await @@ -229,7 +223,7 @@ impl Token<'_> { recipient_ipk: IncomingViewingPublicKey, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let (instruction_data, program, tx_pre_check) = token_program_preparation_transfer(amount); + let (instruction_data, program) = token_program_preparation_transfer(amount); self.0 .send_privacy_preserving_tx( @@ -241,7 +235,6 @@ impl Token<'_> { }, ], &instruction_data, - tx_pre_check, &program, ) .await @@ -255,13 +248,7 @@ impl Token<'_> { } } -fn token_program_preparation_transfer( - amount: u128, -) -> ( - InstructionData, - Program, - impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, -) { +fn token_program_preparation_transfer(amount: u128) -> (InstructionData, Program) { // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || // 0x00 || 0x00 || 0x00]. let mut instruction = [0; 23]; @@ -269,26 +256,20 @@ fn token_program_preparation_transfer( instruction[1..17].copy_from_slice(&amount.to_le_bytes()); let instruction_data = Program::serialize_instruction(instruction).unwrap(); let program = Program::token(); - let tx_pre_check = |_: &[&Account]| Ok(()); - (instruction_data, program, tx_pre_check) + (instruction_data, program) } fn token_program_preparation_definition( name: [u8; 6], total_supply: u128, -) -> ( - InstructionData, - Program, - impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, -) { +) -> (InstructionData, Program) { // 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 instruction_data = Program::serialize_instruction(instruction).unwrap(); let program = Program::token(); - let tx_pre_check = |_: &[&Account]| Ok(()); - (instruction_data, program, tx_pre_check) + (instruction_data, program) } diff --git a/wallet/src/transaction_utils.rs b/wallet/src/transaction_utils.rs deleted file mode 100644 index 10854d9..0000000 --- a/wallet/src/transaction_utils.rs +++ /dev/null @@ -1,117 +0,0 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; -use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; -use nssa::{ - AccountId, PrivacyPreservingTransaction, - privacy_preserving_transaction::{circuit, message::Message, witness_set::WitnessSet}, - program::Program, -}; -use nssa_core::{ - MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - account::AccountWithMetadata, encryption::IncomingViewingPublicKey, -}; - -use crate::{WalletCore, helperfunctions::produce_random_nonces}; - -pub(crate) struct AccountPreparedData { - pub nsk: Option, - pub npk: NullifierPublicKey, - pub ipk: IncomingViewingPublicKey, - pub auth_acc: AccountWithMetadata, - pub proof: Option, -} - -impl WalletCore { - pub(crate) async fn private_acc_preparation( - &self, - account_id: AccountId, - is_authorized: bool, - needs_proof: bool, - ) -> Result { - let Some((from_keys, from_acc)) = self - .storage - .user_data - .get_private_account(&account_id) - .cloned() - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let mut nsk = None; - let mut proof = None; - - let from_npk = from_keys.nullifer_public_key; - let from_ipk = from_keys.incoming_viewing_public_key; - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), is_authorized, &from_npk); - - if is_authorized { - nsk = Some(from_keys.private_key_holder.nullifier_secret_key); - } - - if needs_proof { - // TODO: Remove this unwrap, error types must be compatible - proof = self - .check_private_account_initialized(&account_id) - .await - .unwrap(); - } - - Ok(AccountPreparedData { - nsk, - npk: from_npk, - ipk: from_ipk, - auth_acc: sender_pre, - proof, - }) - } - - // TODO: Remove - pub async fn register_account_under_authenticated_transfers_programs_private( - &self, - from: AccountId, - ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: _, - npk: from_npk, - ipk: from_ipk, - auth_acc: sender_pre, - proof: _, - } = self.private_acc_preparation(from, false, false).await?; - - let eph_holder_from = EphemeralKeyHolder::new(&from_npk); - let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_ipk); - - let instruction: u128 = 0; - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre], - &Program::serialize_instruction(instruction).unwrap(), - &[2], - &produce_random_nonces(1), - &[(from_npk.clone(), shared_secret_from.clone())], - &[], - &Program::authenticated_transfer_program(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![( - from_npk.clone(), - from_ipk.clone(), - eph_holder_from.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_tx_private(tx).await?, - [shared_secret_from], - )) - } -} From 2cf60fff10f35e11154a14d9e1f38834944f3b1b Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 02:37:43 +0300 Subject: [PATCH 26/70] feat: add transaction output after pinata call --- wallet/src/cli/programs/pinata.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index cabee4c..d7d974b 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -1,7 +1,6 @@ use anyhow::Result; use clap::Subcommand; use common::{PINATA_BASE58, transaction::NSSATransaction}; -use log::info; use crate::{ WalletCore, @@ -125,7 +124,19 @@ impl WalletSubcommand for PinataProgramSubcommandPublic { solution, ) .await?; - info!("Results of tx send is {res:#?}"); + + println!("Results of tx send is {res:#?}"); + + let tx_hash = res.tx_hash; + let transfer_tx = wallet_core + .poll_native_token_transfer(tx_hash.clone()) + .await?; + + println!("Transaction data is {transfer_tx:?}"); + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); Ok(SubcommandReturnValue::Empty) } @@ -151,13 +162,15 @@ impl WalletSubcommand for PinataProgramSubcommandPrivate { .claim_private_owned_account(pinata_account_id, winner_account_id, solution) .await?; - info!("Results of tx send is {res:#?}"); + println!("Results of tx send is {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core .poll_native_token_transfer(tx_hash.clone()) .await?; + println!("Transaction data is {transfer_tx:?}"); + if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { let acc_decode_data = vec![(secret_winner, winner_account_id)]; From 5801073f84bdaff744495da70f51c63cfe55a19b Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 03:16:47 +0300 Subject: [PATCH 27/70] feat: compute pinata solution in wallet --- integration_tests/src/test_suite_map.rs | 12 +-- nssa/src/state.rs | 4 +- wallet/Cargo.toml | 1 + .../src/cli/programs/native_token_transfer.rs | 16 ++-- wallet/src/cli/programs/pinata.rs | 88 +++++++++++++------ wallet/src/cli/programs/token.rs | 12 +-- 6 files changed, 81 insertions(+), 52 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 4bdf37f..1801b4c 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1379,10 +1379,8 @@ pub fn prepare_function_map() -> HashMap { let pinata_account_id = PINATA_BASE58; let pinata_prize = 150; - let solution = 989106; let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to_account_id: make_public_account_input_from_str(ACC_SENDER), - solution, + to: make_public_account_input_from_str(ACC_SENDER), }); let wallet_config = fetch_config().await.unwrap(); @@ -1508,11 +1506,9 @@ pub fn prepare_function_map() -> HashMap { info!("########## test_pinata_private_receiver ##########"); let pinata_account_id = PINATA_BASE58; let pinata_prize = 150; - let solution = 989106; let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to_account_id: make_private_account_input_from_str(ACC_SENDER_PRIVATE), - solution, + to: make_private_account_input_from_str(ACC_SENDER_PRIVATE), }); let wallet_config = fetch_config().await.unwrap(); @@ -1565,7 +1561,6 @@ pub fn prepare_function_map() -> HashMap { info!("########## test_pinata_private_receiver ##########"); let pinata_account_id = PINATA_BASE58; let pinata_prize = 150; - let solution = 989106; // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { @@ -1580,8 +1575,7 @@ pub fn prepare_function_map() -> HashMap { }; let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to_account_id: make_private_account_input_from_str(&winner_account_id.to_string()), - solution, + to: make_private_account_input_from_str(&winner_account_id.to_string()), }); let wallet_config = fetch_config().await.unwrap(); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index cef7791..5434636 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -233,8 +233,8 @@ impl V02State { Account { program_owner: Program::pinata().id(), balance: 1500, - // Difficulty: 3 - data: vec![3; 33], + // Difficulty: 2 + data: vec![2; 33], nonce: 0, }, ); diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 74eb5bc..3b12d8f 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -20,6 +20,7 @@ base58.workspace = true hex = "0.4.3" rand.workspace = true itertools = "0.14.0" +sha2.workspace = true [dependencies.key_protocol] path = "../key_protocol" diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 00940b3..9dc72ae 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -61,7 +61,7 @@ impl WalletSubcommand for AuthTransferSubcommand { .register_account(account_id) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?; @@ -79,7 +79,7 @@ impl WalletSubcommand for AuthTransferSubcommand { .register_account_private(account_id) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -320,7 +320,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { .send_private_transfer_to_owned_account(from, to, amount) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -364,7 +364,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { .send_private_transfer_to_outer_account(from, to_npk, to_ipk, amount) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -404,7 +404,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { .send_shielded_transfer(from, to, amount) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -449,7 +449,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { .send_shielded_transfer_to_outer_account(from, to_npk, to_ipk, amount) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; @@ -483,7 +483,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { .send_deshielded_transfer(from, to, amount) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -513,7 +513,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { .send_public_transfer(from, to, amount) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?; diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index d7d974b..b920f3f 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use clap::Subcommand; use common::{PINATA_BASE58, transaction::NSSATransaction}; @@ -14,12 +14,9 @@ use crate::{ pub enum PinataProgramAgnosticSubcommand { /// Claim pinata Claim { - /// to_account_id - valid 32 byte base58 string with privacy prefix + /// to - valid 32 byte base58 string with privacy prefix #[arg(long)] - to_account_id: String, - /// solution - solution to pinata challenge - #[arg(long)] - solution: u128, + to: String, }, } @@ -29,26 +26,20 @@ impl WalletSubcommand for PinataProgramAgnosticSubcommand { wallet_core: &mut WalletCore, ) -> Result { let underlying_subcommand = match self { - PinataProgramAgnosticSubcommand::Claim { - to_account_id, - solution, - } => { - let (to_account_id, to_addr_privacy) = - parse_addr_with_privacy_prefix(&to_account_id)?; + PinataProgramAgnosticSubcommand::Claim { to } => { + let (to, to_addr_privacy) = parse_addr_with_privacy_prefix(&to)?; match to_addr_privacy { AccountPrivacyKind::Public => { PinataProgramSubcommand::Public(PinataProgramSubcommandPublic::Claim { pinata_account_id: PINATA_BASE58.to_string(), - winner_account_id: to_account_id, - solution, + winner_account_id: to, }) } AccountPrivacyKind::Private => PinataProgramSubcommand::Private( PinataProgramSubcommandPrivate::ClaimPrivateOwned { pinata_account_id: PINATA_BASE58.to_string(), - winner_account_id: to_account_id, - solution, + winner_account_id: to, }, ), } @@ -82,9 +73,6 @@ pub enum PinataProgramSubcommandPublic { /// winner_account_id - valid 32 byte hex string #[arg(long)] winner_account_id: String, - /// solution - solution to pinata challenge - #[arg(long)] - solution: u128, }, } @@ -100,9 +88,6 @@ pub enum PinataProgramSubcommandPrivate { /// winner_account_id - valid 32 byte hex string #[arg(long)] winner_account_id: String, - /// solution - solution to pinata challenge - #[arg(long)] - solution: u128, }, } @@ -115,17 +100,21 @@ impl WalletSubcommand for PinataProgramSubcommandPublic { PinataProgramSubcommandPublic::Claim { pinata_account_id, winner_account_id, - solution, } => { + let pinata_account_id = pinata_account_id.parse().unwrap(); + let solution = find_solution(wallet_core, pinata_account_id) + .await + .context("failed to compute solution")?; + let res = Pinata(wallet_core) .claim( - pinata_account_id.parse().unwrap(), + pinata_account_id, winner_account_id.parse().unwrap(), solution, ) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -153,16 +142,18 @@ impl WalletSubcommand for PinataProgramSubcommandPrivate { PinataProgramSubcommandPrivate::ClaimPrivateOwned { pinata_account_id, winner_account_id, - solution, } => { let pinata_account_id = pinata_account_id.parse().unwrap(); let winner_account_id = winner_account_id.parse().unwrap(); + let solution = find_solution(wallet_core, pinata_account_id) + .await + .context("failed to compute solution")?; let (res, secret_winner) = Pinata(wallet_core) .claim_private_owned_account(pinata_account_id, winner_account_id, solution) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -205,3 +196,46 @@ impl WalletSubcommand for PinataProgramSubcommand { } } } + +async fn find_solution(wallet: &WalletCore, pinata_account_id: nssa::AccountId) -> Result { + let account = wallet.get_account_public(pinata_account_id).await?; + let data: [u8; 33] = account + .data + .try_into() + .map_err(|_| anyhow::Error::msg("invalid pinata account data"))?; + + println!("Computing solution for pinata..."); + let now = std::time::Instant::now(); + + let solution = compute_solution(data); + + println!("Found solution {solution} in {:?}", now.elapsed()); + Ok(solution) +} + +fn compute_solution(data: [u8; 33]) -> u128 { + let difficulty = data[0]; + let seed = &data[1..]; + + let mut solution = 0u128; + while !validate_solution(difficulty, seed, solution) { + solution = solution.checked_add(1).expect("solution overflowed u128"); + } + + solution +} + +fn validate_solution(difficulty: u8, seed: &[u8], solution: u128) -> bool { + use sha2::{Digest as _, digest::FixedOutput as _}; + + let mut bytes = [0; 32 + 16]; + bytes[..32].copy_from_slice(seed); + bytes[32..].copy_from_slice(&solution.to_le_bytes()); + + let mut hasher = sha2::Sha256::new(); + hasher.update(bytes); + let digest: [u8; 32] = hasher.finalize_fixed().into(); + + let difficulty = difficulty as usize; + digest[..difficulty].iter().all(|&b| b == 0) +} diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 1fceb74..d1a27dd 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -399,7 +399,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { ) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -437,7 +437,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { ) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -490,7 +490,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { ) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -538,7 +538,7 @@ impl WalletSubcommand for TokenProgramSubcommandDeshielded { ) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -598,7 +598,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { ) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core @@ -631,7 +631,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { ) .await?; - println!("Results of tx send is {res:#?}"); + println!("Results of tx send are {res:#?}"); let tx_hash = res.tx_hash; let transfer_tx = wallet_core From b1b7bc446376bfc33e97ca058a11d47257709fd6 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Mon, 1 Dec 2025 14:19:25 +0200 Subject: [PATCH 28/70] fix: fmt --- wallet/src/lib.rs | 3 ++- wallet/src/main.rs | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 53d1083..23e13d0 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -19,7 +19,8 @@ use tokio::io::AsyncWriteExt; use crate::{ config::PersistentStorage, helperfunctions::{ - fetch_config, fetch_persistent_storage, get_home, produce_data_for_storage, produce_random_nonces + fetch_config, fetch_persistent_storage, get_home, produce_data_for_storage, + produce_random_nonces, }, poller::TxPoller, }; diff --git a/wallet/src/main.rs b/wallet/src/main.rs index e24a2de..71d7620 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -1,8 +1,10 @@ use anyhow::Result; use clap::{CommandFactory as _, Parser as _}; use tokio::runtime::Builder; -use wallet::cli::{Args, OverCommand, execute_continuous_run, execute_subcommand}; -use wallet::execute_setup; +use wallet::{ + cli::{Args, OverCommand, execute_continuous_run, execute_subcommand}, + execute_setup, +}; pub const NUM_THREADS: usize = 2; @@ -31,9 +33,7 @@ fn main() -> Result<()> { let _output = execute_subcommand(command).await?; Ok(()) } - OverCommand::Setup { password } => { - Ok(execute_setup(password).await?) - } + OverCommand::Setup { password } => Ok(execute_setup(password).await?), } } else if args.continuous_run { Ok(execute_continuous_run().await?) From 2fd4a37ee4c75143ad9874044da069354a114130 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 2 Dec 2025 15:27:20 +0200 Subject: [PATCH 29/70] fix: private definition support --- integration_tests/src/test_suite_map.rs | 157 +++++++++- wallet/src/cli/programs/token.rs | 368 +++++++++++++++++------- wallet/src/program_facades/token.rs | 59 +++- 3 files changed, 475 insertions(+), 109 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 9903345..2819d03 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -444,8 +444,8 @@ pub fn prepare_function_map() -> HashMap { /// test executes a private token transfer to a new account. All accounts are owned except /// definition. #[nssa_integration_test] - pub async fn test_success_token_program_private_owned() { - info!("########## test_success_token_program_private_owned ##########"); + pub async fn test_success_token_program_private_owned_supply() { + info!("########## test_success_token_program_private_owned_supply ##########"); let wallet_config = fetch_config().await.unwrap(); // Create new account for the token definition (public) @@ -602,6 +602,159 @@ pub fn prepare_function_map() -> HashMap { assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await); } + /// This test creates a new private token using the token program. All accounts are owned except + /// suply. + #[nssa_integration_test] + pub async fn test_success_token_program_private_owned_definition() { + info!("########## test_success_token_program_private_owned_definition ##########"); + let wallet_config = fetch_config().await.unwrap(); + + // Create new account for the token definition (public) + let SubcommandReturnValue::RegisterAccount { + account_id: definition_account_id, + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private { + cci: ChainIndex::root(), + }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + // Create new account for the token supply holder (private) + let SubcommandReturnValue::RegisterAccount { + account_id: supply_account_id, + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public { + cci: ChainIndex::root(), + }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + + // Create new token + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_account_id: make_private_account_input_from_str( + &definition_account_id.to_string(), + ), + supply_account_id: make_public_account_input_from_str(&supply_account_id.to_string()), + name: "A NAME".to_string(), + total_supply: 37, + }; + + wallet::cli::execute_subcommand(Command::Token(subcommand)) + .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(); + + let wallet_config = fetch_config().await.unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + let new_commitment1 = wallet_storage + .get_private_account_commitment(&definition_account_id) + .unwrap(); + assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await); + + // Check the status of the token definition account is the expected after the execution + let supply_acc = seq_client + .get_account(supply_account_id.to_string()) + .await + .unwrap() + .account; + + assert_eq!(supply_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!( + supply_acc.data, + vec![ + 1, 128, 101, 5, 31, 43, 36, 97, 108, 164, 92, 25, 157, 173, 5, 14, 194, 121, 239, + 84, 19, 160, 243, 47, 193, 2, 250, 247, 232, 253, 191, 232, 173, 37, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + } + + /// This test creates a new private token using the token program. All accounts are owned. + #[nssa_integration_test] + pub async fn test_success_token_program_private_owned_definition_and_supply() { + info!( + "########## test_success_token_program_private_owned_definition_and_supply ##########" + ); + let wallet_config = fetch_config().await.unwrap(); + + // Create new account for the token definition (public) + let SubcommandReturnValue::RegisterAccount { + account_id: definition_account_id, + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private { + cci: ChainIndex::root(), + }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + // Create new account for the token supply holder (private) + let SubcommandReturnValue::RegisterAccount { + account_id: supply_account_id, + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private { + cci: ChainIndex::root(), + }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + + // Create new token + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_account_id: make_private_account_input_from_str( + &definition_account_id.to_string(), + ), + supply_account_id: make_private_account_input_from_str(&supply_account_id.to_string()), + name: "A NAME".to_string(), + total_supply: 37, + }; + + wallet::cli::execute_subcommand(Command::Token(subcommand)) + .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(); + + let wallet_config = fetch_config().await.unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + let new_commitment1 = wallet_storage + .get_private_account_commitment(&definition_account_id) + .unwrap(); + assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await); + + let new_commitment2 = wallet_storage + .get_private_account_commitment(&supply_account_id) + .unwrap(); + assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await); + } + /// This test creates a new private token using the token program. After creating the token, the /// test executes a private token transfer to a new account. #[nssa_integration_test] diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index d1a27dd..4480a1e 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -14,8 +14,6 @@ use crate::{ #[derive(Subcommand, Debug, Clone)] pub enum TokenProgramAgnosticSubcommand { /// Produce a new token - /// - /// Currently the only supported privacy options is for public definition New { /// definition_account_id - valid 32 byte base58 string with privacy prefix #[arg(long)] @@ -72,8 +70,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { let underlying_subcommand = match (definition_addr_privacy, supply_addr_privacy) { (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { - TokenProgramSubcommand::Public( - TokenProgramSubcommandPublic::CreateNewToken { + TokenProgramSubcommand::Create( + CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp { definition_account_id, supply_account_id, name, @@ -82,8 +80,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { ) } (AccountPrivacyKind::Public, AccountPrivacyKind::Private) => { - TokenProgramSubcommand::Private( - TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned { + TokenProgramSubcommand::Create( + CreateNewTokenProgramSubcommand::NewPublicDefPrivateSupp { definition_account_id, supply_account_id, name, @@ -92,14 +90,24 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { ) } (AccountPrivacyKind::Private, AccountPrivacyKind::Private) => { - // ToDo: maybe implement this one. It is not immediately clear why - // definition should be private. - anyhow::bail!("Unavailable privacy pairing") + TokenProgramSubcommand::Create( + CreateNewTokenProgramSubcommand::NewPrivateDefPrivateSupp { + definition_account_id, + supply_account_id, + name, + total_supply, + }, + ) } (AccountPrivacyKind::Private, AccountPrivacyKind::Public) => { - // ToDo: Probably valid. If definition is not public, but supply is it is - // very suspicious. - anyhow::bail!("Unavailable privacy pairing") + TokenProgramSubcommand::Create( + CreateNewTokenProgramSubcommand::NewPrivateDefPublicSupp { + definition_account_id, + supply_account_id, + name, + total_supply, + }, + ) } }; @@ -202,6 +210,9 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { /// Represents generic CLI subcommand for a wallet working with token_program #[derive(Subcommand, Debug, Clone)] pub enum TokenProgramSubcommand { + /// Creation of new token + #[command(subcommand)] + Create(CreateNewTokenProgramSubcommand), /// Public execution #[command(subcommand)] Public(TokenProgramSubcommandPublic), @@ -219,17 +230,6 @@ pub enum TokenProgramSubcommand { /// Represents generic public CLI subcommand for a wallet working with token_program #[derive(Subcommand, Debug, Clone)] pub enum TokenProgramSubcommandPublic { - // Create a new token using the token program - CreateNewToken { - #[arg(short, long)] - definition_account_id: String, - #[arg(short, long)] - supply_account_id: String, - #[arg(short, long)] - name: String, - #[arg(short, long)] - total_supply: u128, - }, // Transfer tokens using the token program TransferToken { #[arg(short, long)] @@ -244,17 +244,6 @@ pub enum TokenProgramSubcommandPublic { /// Represents generic private CLI subcommand for a wallet working with token_program #[derive(Subcommand, Debug, Clone)] pub enum TokenProgramSubcommandPrivate { - // Create a new token using the token program - CreateNewTokenPrivateOwned { - #[arg(short, long)] - definition_account_id: String, - #[arg(short, long)] - supply_account_id: String, - #[arg(short, long)] - name: String, - #[arg(short, long)] - total_supply: u128, - }, // Transfer tokens using the token program TransferTokenPrivateOwned { #[arg(short, long)] @@ -320,35 +309,69 @@ pub enum TokenProgramSubcommandShielded { }, } +/// Represents generic initialization subcommand for a wallet working with token_program +#[derive(Subcommand, Debug, Clone)] +pub enum CreateNewTokenProgramSubcommand { + /// Create a new token using the token program + /// + /// Definition - public, supply - public + NewPublicDefPublicSupp { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + supply_account_id: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + total_supply: u128, + }, + /// Create a new token using the token program + /// + /// Definition - public, supply - private + NewPublicDefPrivateSupp { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + supply_account_id: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + total_supply: u128, + }, + /// Create a new token using the token program + /// + /// Definition - private, supply - public + NewPrivateDefPublicSupp { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + supply_account_id: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + total_supply: u128, + }, + /// Create a new token using the token program + /// + /// Definition - private, supply - private + NewPrivateDefPrivateSupp { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + supply_account_id: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + total_supply: u128, + }, +} + impl WalletSubcommand for TokenProgramSubcommandPublic { async fn handle_subcommand( self, wallet_core: &mut WalletCore, ) -> Result { match self { - TokenProgramSubcommandPublic::CreateNewToken { - definition_account_id, - supply_account_id, - 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); - Token(wallet_core) - .send_new_definition( - definition_account_id.parse().unwrap(), - supply_account_id.parse().unwrap(), - name_bytes, - total_supply, - ) - .await?; - Ok(SubcommandReturnValue::Empty) - } TokenProgramSubcommandPublic::TransferToken { sender_account_id, recipient_account_id, @@ -373,54 +396,6 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { wallet_core: &mut WalletCore, ) -> Result { match self { - TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned { - definition_account_id, - supply_account_id, - name, - total_supply, - } => { - let name = name.as_bytes(); - if name.len() > 6 { - // TODO: return error - panic!("Name length mismatch"); - } - let mut name_bytes = [0; 6]; - name_bytes[..name.len()].copy_from_slice(name); - - let definition_account_id: AccountId = definition_account_id.parse().unwrap(); - let supply_account_id: AccountId = supply_account_id.parse().unwrap(); - - let (res, secret_supply) = Token(wallet_core) - .send_new_definition_private_owned( - definition_account_id, - supply_account_id, - name_bytes, - total_supply, - ) - .await?; - - println!("Results of tx send are {res:#?}"); - - let tx_hash = res.tx_hash; - let transfer_tx = wallet_core - .poll_native_token_transfer(tx_hash.clone()) - .await?; - - if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_supply, supply_account_id)]; - - wallet_core.decode_insert_privacy_preserving_transaction_results( - tx, - &acc_decode_data, - )?; - } - - let path = wallet_core.store_persistent_data().await?; - - println!("Stored persistent accounts at {path:#?}"); - - Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) - } TokenProgramSubcommandPrivate::TransferTokenPrivateOwned { sender_account_id, recipient_account_id, @@ -657,12 +632,195 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { } } +impl WalletSubcommand for CreateNewTokenProgramSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + CreateNewTokenProgramSubcommand::NewPrivateDefPrivateSupp { + definition_account_id, + supply_account_id, + name, + total_supply, + } => { + let name = name.as_bytes(); + if name.len() > 6 { + // TODO: return error + panic!("Name length mismatch"); + } + let mut name_bytes = [0; 6]; + name_bytes[..name.len()].copy_from_slice(name); + + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let supply_account_id: AccountId = supply_account_id.parse().unwrap(); + + let (res, [secret_definition, secret_supply]) = Token(wallet_core) + .send_new_definition_private_owned_definiton_and_supply( + definition_account_id, + supply_account_id, + name_bytes, + total_supply, + ) + .await?; + + println!("Results of tx send are {res:#?}"); + + let tx_hash = res.tx_hash; + let transfer_tx = wallet_core + .poll_native_token_transfer(tx_hash.clone()) + .await?; + + if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { + let acc_decode_data = vec![ + (secret_definition, definition_account_id), + (secret_supply, supply_account_id), + ]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + CreateNewTokenProgramSubcommand::NewPrivateDefPublicSupp { + definition_account_id, + supply_account_id, + name, + total_supply, + } => { + let name = name.as_bytes(); + if name.len() > 6 { + // TODO: return error + panic!("Name length mismatch"); + } + let mut name_bytes = [0; 6]; + name_bytes[..name.len()].copy_from_slice(name); + + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let supply_account_id: AccountId = supply_account_id.parse().unwrap(); + + let (res, secret_definition) = Token(wallet_core) + .send_new_definition_private_owned_definiton( + definition_account_id, + supply_account_id, + name_bytes, + total_supply, + ) + .await?; + + println!("Results of tx send are {res:#?}"); + + let tx_hash = res.tx_hash; + let transfer_tx = wallet_core + .poll_native_token_transfer(tx_hash.clone()) + .await?; + + if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { + let acc_decode_data = vec![(secret_definition, definition_account_id)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + CreateNewTokenProgramSubcommand::NewPublicDefPrivateSupp { + definition_account_id, + supply_account_id, + name, + total_supply, + } => { + let name = name.as_bytes(); + if name.len() > 6 { + // TODO: return error + panic!("Name length mismatch"); + } + let mut name_bytes = [0; 6]; + name_bytes[..name.len()].copy_from_slice(name); + + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let supply_account_id: AccountId = supply_account_id.parse().unwrap(); + + let (res, secret_supply) = Token(wallet_core) + .send_new_definition_private_owned_supply( + definition_account_id, + supply_account_id, + name_bytes, + total_supply, + ) + .await?; + + println!("Results of tx send are {res:#?}"); + + let tx_hash = res.tx_hash; + let transfer_tx = wallet_core + .poll_native_token_transfer(tx_hash.clone()) + .await?; + + if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { + let acc_decode_data = vec![(secret_supply, supply_account_id)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp { + definition_account_id, + supply_account_id, + 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); + Token(wallet_core) + .send_new_definition( + definition_account_id.parse().unwrap(), + supply_account_id.parse().unwrap(), + name_bytes, + total_supply, + ) + .await?; + Ok(SubcommandReturnValue::Empty) + } + } + } +} + impl WalletSubcommand for TokenProgramSubcommand { async fn handle_subcommand( self, wallet_core: &mut WalletCore, ) -> Result { match self { + TokenProgramSubcommand::Create(creation_subcommand) => { + creation_subcommand.handle_subcommand(wallet_core).await + } TokenProgramSubcommand::Private(private_subcommand) => { private_subcommand.handle_subcommand(wallet_core).await } diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 298c4f4..0cc1ec4 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -38,7 +38,7 @@ impl Token<'_> { Ok(self.0.sequencer_client.send_tx_public(tx).await?) } - pub async fn send_new_definition_private_owned( + pub async fn send_new_definition_private_owned_supply( &self, definition_account_id: AccountId, supply_account_id: AccountId, @@ -61,11 +61,66 @@ impl Token<'_> { let first = secrets .into_iter() .next() - .expect("expected recipient's secret"); + .expect("expected supply's secret"); (resp, first) }) } + pub async fn send_new_definition_private_owned_definiton( + &self, + definition_account_id: AccountId, + supply_account_id: AccountId, + name: [u8; 6], + total_supply: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_definition(name, total_supply); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(definition_account_id), + PrivacyPreservingAccount::Public(supply_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected definition's secret"); + (resp, first) + }) + } + + pub async fn send_new_definition_private_owned_definiton_and_supply( + &self, + definition_account_id: AccountId, + supply_account_id: AccountId, + name: [u8; 6], + total_supply: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_definition(name, total_supply); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(definition_account_id), + PrivacyPreservingAccount::PrivateOwned(supply_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected definition's secret"); + let second = iter.next().expect("expected supply's secret"); + (resp, [first, second]) + }) + } + pub async fn send_transfer_transaction( &self, sender_account_id: AccountId, From 5affa4f9fdd03c9dacef2e734342b91f76867aa3 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 3 Dec 2025 07:05:23 +0200 Subject: [PATCH 30/70] fix: suggestions fix --- integration_tests/src/test_suite_map.rs | 127 +++++++++++++----- .../src/key_management/key_tree/mod.rs | 20 +++ 2 files changed, 114 insertions(+), 33 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 9f61810..6af300a 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1722,7 +1722,7 @@ pub fn prepare_function_map() -> HashMap { )), to_npk: None, to_ipk: None, - amount: 100, + amount: 101, }); wallet::execute_subcommand(command).await.unwrap(); @@ -1760,7 +1760,7 @@ pub fn prepare_function_map() -> HashMap { )), to_npk: None, to_ipk: None, - amount: 100, + amount: 102, }); wallet::execute_subcommand(command).await.unwrap(); @@ -1772,7 +1772,7 @@ pub fn prepare_function_map() -> HashMap { )), to_npk: None, to_ipk: None, - amount: 100, + amount: 103, }); wallet::execute_subcommand(command).await.unwrap(); @@ -1788,39 +1788,100 @@ pub fn prepare_function_map() -> HashMap { .await .unwrap(); - assert!( - wallet_storage - .storage - .user_data - .private_key_tree - .get_node(to_account_id1) - .is_some() + let acc1 = wallet_storage + .storage + .user_data + .private_key_tree + .get_node(to_account_id1) + .expect("Acc 1 should be restored"); + + let acc2 = wallet_storage + .storage + .user_data + .private_key_tree + .get_node(to_account_id2) + .expect("Acc 2 should be restored"); + + let _ = wallet_storage + .storage + .user_data + .public_key_tree + .get_node(to_account_id3) + .expect("Acc 3 should be restored"); + + let _ = wallet_storage + .storage + .user_data + .public_key_tree + .get_node(to_account_id4) + .expect("Acc 4 should be restored"); + + assert_eq!( + acc1.value.1.program_owner, + Program::authenticated_transfer_program().id() ); - assert!( - wallet_storage - .storage - .user_data - .private_key_tree - .get_node(to_account_id2) - .is_some() - ); - assert!( - wallet_storage - .storage - .user_data - .public_key_tree - .get_node(to_account_id3) - .is_some() - ); - assert!( - wallet_storage - .storage - .user_data - .public_key_tree - .get_node(to_account_id4) - .is_some() + assert_eq!( + acc2.value.1.program_owner, + Program::authenticated_transfer_program().id() ); + assert_eq!(acc1.value.1.balance, 100); + assert_eq!(acc2.value.1.balance, 101); + + info!("########## TREE CHECKS END ##########"); + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_private_account_input_from_str(&to_account_id1.to_string()), + to: Some(make_private_account_input_from_str( + &to_account_id2.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 10, + }); + + wallet::execute_subcommand(command).await.unwrap(); + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(&to_account_id3.to_string()), + to: Some(make_public_account_input_from_str( + &to_account_id4.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 11, + }); + + wallet::execute_subcommand(command).await.unwrap(); + + let wallet_config = fetch_config().await.unwrap(); + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config.clone()) + .await + .unwrap(); + + let comm1 = wallet_storage + .get_private_account_commitment(&to_account_id1) + .expect("Acc 1 commitment should exist"); + let comm2 = wallet_storage + .get_private_account_commitment(&to_account_id2) + .expect("Acc 2 commitment should exist"); + + assert!(verify_commitment_is_in_state(comm1, &seq_client).await); + assert!(verify_commitment_is_in_state(comm2, &seq_client).await); + + let acc3 = seq_client + .get_account_balance(to_account_id3.to_string()) + .await + .expect("Acc 3 must be present in public state"); + let acc4 = seq_client + .get_account_balance(to_account_id4.to_string()) + .await + .expect("Acc 4 must be present in public state"); + + assert_eq!(acc3.balance, 91); + assert_eq!(acc4.balance, 114); + info!("Success!"); } diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 7f91143..3d55516 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -140,6 +140,12 @@ impl KeyTree { self.key_map.remove(&chain_index) } + /// Populates tree with children. + /// + /// For given `depth` adds children to a tree such that their `ChainIndex::depth(&self) < + /// depth`. + /// + /// Tree must be empty before start pub fn generate_tree_for_depth(&mut self, depth: u32) { let mut id_stack = vec![ChainIndex::root()]; @@ -157,6 +163,14 @@ impl KeyTree { } impl KeyTree { + /// Cleanup of all non-initialized accounts in a private tree + /// + /// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) < + /// depth`. + /// + /// If account is default, removes them. + /// + /// Chain must be parsed for accounts beforehand pub fn cleanup_tree_for_depth(&mut self, depth: u32) { let mut id_stack = vec![ChainIndex::root()]; @@ -180,6 +194,12 @@ impl KeyTree { } impl KeyTree { + /// Cleanup of all non-initialized accounts in a public tree + /// + /// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) < + /// depth`. + /// + /// If account is default, removes them. pub async fn cleanup_tree_for_depth( &mut self, depth: u32, From b81aafbc2334ba6c45c3aa0347ddf4789957d074 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 3 Dec 2025 13:10:07 +0200 Subject: [PATCH 31/70] feat: UX improvements --- integration_tests/src/test_suite_map.rs | 85 ++++--------- .../src/key_management/key_tree/mod.rs | 114 ++++++++++++++---- key_protocol/src/key_protocol_core/mod.rs | 37 ++++-- wallet/src/cli/account.rs | 14 ++- wallet/src/cli/config.rs | 9 -- wallet/src/cli/mod.rs | 48 +++++--- wallet/src/lib.rs | 10 +- wallet/src/main.rs | 19 +-- 8 files changed, 192 insertions(+), 144 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 8eb849f..d5bed4c 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -87,9 +87,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_move_to_another_account() { info!("########## test_success_move_to_another_account ##########"); - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: ChainIndex::root(), - })); + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })); let wallet_config = fetch_config().await.unwrap(); @@ -293,9 +291,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -306,9 +302,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -319,9 +313,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -453,9 +445,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -466,9 +456,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private { - cci: ChainIndex::root(), - }, + NewSubcommand::Private { cci: None }, ))) .await .unwrap() @@ -479,9 +467,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private { - cci: ChainIndex::root(), - }, + NewSubcommand::Private { cci: None }, ))) .await .unwrap() @@ -614,9 +600,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -627,9 +611,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private { - cci: ChainIndex::root(), - }, + NewSubcommand::Private { cci: None }, ))) .await .unwrap() @@ -640,9 +622,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private { - cci: ChainIndex::root(), - }, + NewSubcommand::Private { cci: None }, ))) .await .unwrap() @@ -756,9 +736,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -769,9 +747,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -782,9 +758,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private { - cci: ChainIndex::root(), - }, + NewSubcommand::Private { cci: None }, ))) .await .unwrap() @@ -898,9 +872,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -911,9 +883,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private { - cci: ChainIndex::root(), - }, + NewSubcommand::Private { cci: None }, ))) .await .unwrap() @@ -924,9 +894,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: recipient_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public { - cci: ChainIndex::root(), - }, + NewSubcommand::Public { cci: None }, ))) .await .unwrap() @@ -1127,9 +1095,8 @@ pub fn prepare_function_map() -> HashMap { ); let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap(); - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: ChainIndex::root(), - })); + let command = + Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None })); let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); let SubcommandReturnValue::RegisterAccount { @@ -1490,9 +1457,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_authenticated_transfer_initialize_function() { info!("########## test initialize account for authenticated transfer ##########"); - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: ChainIndex::root(), - })); + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None })); let SubcommandReturnValue::RegisterAccount { account_id } = wallet::cli::execute_subcommand(command).await.unwrap() else { @@ -1590,9 +1555,7 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { account_id: winner_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private { - cci: ChainIndex::root(), - }, + NewSubcommand::Private { cci: None }, ))) .await .unwrap() @@ -1676,7 +1639,7 @@ pub fn prepare_function_map() -> HashMap { let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap(); let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: ChainIndex::root(), + cci: Some(ChainIndex::root()), })); let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); @@ -1688,7 +1651,7 @@ pub fn prepare_function_map() -> HashMap { }; let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: ChainIndex::from_str("/0").unwrap(), + cci: Some(ChainIndex::from_str("/0").unwrap()), })); let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); @@ -1726,7 +1689,7 @@ pub fn prepare_function_map() -> HashMap { let from: AccountId = ACC_SENDER.parse().unwrap(); let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: ChainIndex::root(), + cci: Some(ChainIndex::root()), })); let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); @@ -1738,7 +1701,7 @@ pub fn prepare_function_map() -> HashMap { }; let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: ChainIndex::from_str("/0").unwrap(), + cci: Some(ChainIndex::from_str("/0").unwrap()), })); let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap(); diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 3d55516..beb0f95 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, VecDeque}, sync::Arc, }; @@ -20,6 +20,8 @@ pub mod keys_private; pub mod keys_public; pub mod traits; +pub const DEPTH_SOFT_CAP: u32 = 20; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct KeyTree { pub key_map: BTreeMap, @@ -101,10 +103,13 @@ impl KeyTree { } } - pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option { - let father_keys = self.key_map.get(&parent_cci)?; + pub fn generate_new_node( + &mut self, + parent_cci: &ChainIndex, + ) -> Option<(nssa::AccountId, ChainIndex)> { + let father_keys = self.key_map.get(parent_cci)?; let next_child_id = self - .find_next_last_child_of_id(&parent_cci) + .find_next_last_child_of_id(parent_cci) .expect("Can be None only if parent is not present"); let next_cci = parent_cci.nth_child(next_child_id); @@ -113,9 +118,43 @@ impl KeyTree { let account_id = child_keys.account_id(); self.key_map.insert(next_cci.clone(), child_keys); - self.account_id_map.insert(account_id, next_cci); + self.account_id_map.insert(account_id, next_cci.clone()); - Some(account_id) + Some((account_id, next_cci)) + } + + fn have_child_slot_capped(&self, cci: &ChainIndex) -> bool { + let depth = cci.depth(); + + self.find_next_last_child_of_id(cci) + .map(|inn| inn + 1 + depth < DEPTH_SOFT_CAP) + .unwrap_or(false) + } + + pub fn search_new_parent_capped(&self) -> Option { + let mut parent_list = VecDeque::new(); + parent_list.push_front(ChainIndex::root()); + + let mut search_res = None; + + while let Some(next_parent) = parent_list.pop_back() { + if self.have_child_slot_capped(&next_parent) { + search_res = Some(next_parent); + break; + } else { + let last_child = self.find_next_last_child_of_id(&next_parent)?; + + for id in 0..last_child { + parent_list.push_front(next_parent.nth_child(id)); + } + } + } + + search_res + } + + pub fn generate_new_node_capped(&mut self) -> Option<(nssa::AccountId, ChainIndex)> { + self.generate_new_node(&self.search_new_parent_capped()?) } pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> { @@ -150,7 +189,7 @@ impl KeyTree { let mut id_stack = vec![ChainIndex::root()]; while let Some(curr_id) = id_stack.pop() { - self.generate_new_node(curr_id.clone()); + self.generate_new_node(&curr_id); let mut next_id = curr_id.nth_child(0); @@ -268,7 +307,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -281,12 +320,12 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); let next_last_child_for_parent_id = tree .find_next_last_child_of_id(&ChainIndex::root()) @@ -307,7 +346,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -320,7 +359,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - let key_opt = tree.generate_new_node(ChainIndex::from_str("/3").unwrap()); + let key_opt = tree.generate_new_node(&ChainIndex::from_str("/3").unwrap()); assert_eq!(key_opt, None); } @@ -337,7 +376,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -350,7 +389,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -363,7 +402,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 2); - tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -377,7 +416,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/0").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -391,7 +430,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/1").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -405,7 +444,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/2").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("/0/1").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0/1").unwrap()) .unwrap(); assert!( @@ -419,4 +458,35 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); } + + #[test] + fn test_tree_balancing_automatic() { + let seed_holder = seed_holder_for_tests(); + + let mut tree = KeyTreePublic::new(&seed_holder); + + for _ in 0..19 { + tree.generate_new_node_capped().unwrap(); + } + + let next_suitable_parent = tree.search_new_parent_capped().unwrap(); + + assert_eq!(next_suitable_parent, ChainIndex::from_str("/0").unwrap()); + + for _ in 0..18 { + tree.generate_new_node_capped().unwrap(); + } + + let next_suitable_parent = tree.search_new_parent_capped().unwrap(); + + assert_eq!(next_suitable_parent, ChainIndex::from_str("/1").unwrap()); + + for _ in 0..17 { + tree.generate_new_node_capped().unwrap(); + } + + let next_suitable_parent = tree.search_new_parent_capped().unwrap(); + + assert_eq!(next_suitable_parent, ChainIndex::from_str("/2").unwrap()); + } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index ac5ee48..41a686b 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -89,9 +89,18 @@ impl NSSAUserData { /// Returns the account_id of new account pub fn generate_new_public_transaction_private_key( &mut self, - parent_cci: ChainIndex, - ) -> nssa::AccountId { - self.public_key_tree.generate_new_node(parent_cci).unwrap() + parent_cci: Option, + ) -> (nssa::AccountId, ChainIndex) { + match parent_cci { + Some(parent_cci) => self + .public_key_tree + .generate_new_node(&parent_cci) + .expect("Parent must be present in a tree"), + None => self + .public_key_tree + .generate_new_node_capped() + .expect("No slots left"), + } } /// Returns the signing key for public transaction signatures @@ -113,9 +122,18 @@ impl NSSAUserData { /// Returns the account_id of new account pub fn generate_new_privacy_preserving_transaction_key_chain( &mut self, - parent_cci: ChainIndex, - ) -> nssa::AccountId { - self.private_key_tree.generate_new_node(parent_cci).unwrap() + parent_cci: Option, + ) -> (nssa::AccountId, ChainIndex) { + match parent_cci { + Some(parent_cci) => self + .private_key_tree + .generate_new_node(&parent_cci) + .expect("Parent must be present in a tree"), + None => self + .private_key_tree + .generate_new_node_capped() + .expect("No slots left"), + } } /// Returns the signing key for public transaction signatures @@ -169,10 +187,9 @@ mod tests { fn test_new_account() { let mut user_data = NSSAUserData::default(); - let account_id_pub = - user_data.generate_new_public_transaction_private_key(ChainIndex::root()); - let account_id_private = - user_data.generate_new_privacy_preserving_transaction_key_chain(ChainIndex::root()); + let (account_id_pub, _) = user_data.generate_new_public_transaction_private_key(None); + let (account_id_private, _) = + user_data.generate_new_privacy_preserving_transaction_key_chain(None); let is_private_key_generated = user_data .get_pub_account_signing_key(&account_id_pub) diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 5b23b2b..b3c8d5c 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -98,13 +98,13 @@ pub enum NewSubcommand { Public { #[arg(long)] /// Chain index of a parent node - cci: ChainIndex, + cci: Option, }, /// Register new private account Private { #[arg(long)] /// Chain index of a parent node - cci: ChainIndex, + cci: Option, }, } @@ -115,9 +115,11 @@ impl WalletSubcommand for NewSubcommand { ) -> Result { match self { NewSubcommand::Public { cci } => { - let account_id = wallet_core.create_new_account_public(cci); + let (account_id, chain_index) = wallet_core.create_new_account_public(cci); - println!("Generated new account with account_id Public/{account_id}"); + println!( + "Generated new account with account_id Public/{account_id} at path {chain_index}" + ); let path = wallet_core.store_persistent_data().await?; @@ -126,7 +128,7 @@ impl WalletSubcommand for NewSubcommand { Ok(SubcommandReturnValue::RegisterAccount { account_id }) } NewSubcommand::Private { cci } => { - let account_id = wallet_core.create_new_account_private(cci); + let (account_id, chain_index) = wallet_core.create_new_account_private(cci); let (key, _) = wallet_core .storage @@ -135,7 +137,7 @@ impl WalletSubcommand for NewSubcommand { .unwrap(); println!( - "Generated new account with account_id Private/{}", + "Generated new account with account_id Private/{} at path {chain_index}", account_id.to_bytes().to_base58() ); println!("With npk {}", hex::encode(key.nullifer_public_key.0)); diff --git a/wallet/src/cli/config.rs b/wallet/src/cli/config.rs index 68670af..3026c29 100644 --- a/wallet/src/cli/config.rs +++ b/wallet/src/cli/config.rs @@ -9,10 +9,6 @@ use crate::{ /// Represents generic config CLI subcommand #[derive(Subcommand, Debug, Clone)] pub enum ConfigSubcommand { - /// Command to explicitly setup config and storage - /// - /// Does nothing in case if both already present - Setup {}, /// Getter of config fields Get { key: String }, /// Setter of config fields @@ -27,11 +23,6 @@ impl WalletSubcommand for ConfigSubcommand { wallet_core: &mut WalletCore, ) -> Result { match self { - ConfigSubcommand::Setup {} => { - let path = wallet_core.store_persistent_data().await?; - - println!("Stored persistent accounts at {path:#?}"); - } ConfigSubcommand::Get { key } => match key.as_str() { "all" => { let config_str = diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 1f0e53b..e9f76bd 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{io::Write, sync::Arc}; use anyhow::Result; use clap::{Parser, Subcommand}; @@ -16,7 +16,7 @@ use crate::{ token::TokenProgramAgnosticSubcommand, }, }, - helperfunctions::{fetch_config, parse_block_range}, + helperfunctions::{fetch_config, fetch_persistent_storage, parse_block_range}, }; pub mod account; @@ -54,25 +54,12 @@ pub enum Command { /// Command to setup config, get and set config fields #[command(subcommand)] Config(ConfigSubcommand), -} - -/// Represents overarching CLI command for a wallet with setup included -#[derive(Debug, Subcommand, Clone)] -#[clap(about)] -pub enum OverCommand { - /// Represents CLI command for a wallet - #[command(subcommand)] - Command(Command), - /// Setup of a storage. Initializes rots for public and private trees from `password`. - Setup { - #[arg(short, long)] - password: String, - }, + /// Restoring keys from given password at given `depth` + /// /// !!!WARNING!!! will rewrite current storage RestoreKeys { #[arg(short, long)] - password: String, - #[arg(short, long)] + /// Indicates, how deep in tree accounts may be. Affects command complexity. depth: u32, }, } @@ -91,7 +78,7 @@ pub struct Args { pub continuous_run: bool, /// Wallet command #[command(subcommand)] - pub command: Option, + pub command: Option, } #[derive(Debug, Clone)] @@ -104,6 +91,13 @@ pub enum SubcommandReturnValue { } pub async fn execute_subcommand(command: Command) -> Result { + if fetch_persistent_storage().await.is_err() { + println!("Persistent storage not found, need to execute setup"); + + let password = read_password_from_stdin()?; + execute_setup(password).await?; + } + let wallet_config = fetch_config().await?; let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?; @@ -164,6 +158,12 @@ pub async fn execute_subcommand(command: Command) -> Result { + let password = read_password_from_stdin()?; + execute_keys_restoration(password, depth).await?; + + SubcommandReturnValue::Empty + } }; Ok(subcommand_ret) @@ -197,6 +197,16 @@ pub async fn execute_continuous_run() -> Result<()> { } } +pub fn read_password_from_stdin() -> Result { + let mut password = String::new(); + + print!("Input password: "); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut password)?; + + Ok(password.trim().to_string()) +} + pub async fn execute_setup(password: String) -> Result<()> { let config = fetch_config().await?; let wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password).await?; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index f79d947..b2797ed 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -108,13 +108,19 @@ impl WalletCore { Ok(config_path) } - pub fn create_new_account_public(&mut self, chain_index: ChainIndex) -> AccountId { + pub fn create_new_account_public( + &mut self, + chain_index: Option, + ) -> (AccountId, ChainIndex) { self.storage .user_data .generate_new_public_transaction_private_key(chain_index) } - pub fn create_new_account_private(&mut self, chain_index: ChainIndex) -> AccountId { + pub fn create_new_account_private( + &mut self, + chain_index: Option, + ) -> (AccountId, ChainIndex) { self.storage .user_data .generate_new_privacy_preserving_transaction_key_chain(chain_index) diff --git a/wallet/src/main.rs b/wallet/src/main.rs index 00aa6d0..3fcac30 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -1,10 +1,7 @@ use anyhow::Result; use clap::{CommandFactory as _, Parser as _}; use tokio::runtime::Builder; -use wallet::cli::{ - Args, OverCommand, execute_continuous_run, execute_keys_restoration, execute_setup, - execute_subcommand, -}; +use wallet::cli::{Args, execute_continuous_run, execute_subcommand}; pub const NUM_THREADS: usize = 2; @@ -26,17 +23,9 @@ fn main() -> Result<()> { env_logger::init(); runtime.block_on(async move { - if let Some(over_command) = args.command { - match over_command { - OverCommand::Command(command) => { - let _output = execute_subcommand(command).await?; - Ok(()) - } - OverCommand::RestoreKeys { password, depth } => { - execute_keys_restoration(password, depth).await - } - OverCommand::Setup { password } => execute_setup(password).await, - } + if let Some(command) = args.command { + let _output = execute_subcommand(command).await?; + Ok(()) } else if args.continuous_run { execute_continuous_run().await } else { From 2453ae095f9f5e650bc6a6e2df986dca70866e03 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Thu, 4 Dec 2025 14:31:52 +0200 Subject: [PATCH 32/70] fix: cleanup fixing --- .../src/key_management/key_tree/mod.rs | 160 +++++++++++++++--- key_protocol/src/key_protocol_core/mod.rs | 10 +- 2 files changed, 141 insertions(+), 29 deletions(-) diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 3d55516..7b85d8e 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -95,27 +95,29 @@ impl KeyTree { right = (left_border + right) / 2; } (None, Some(_)) => { - unreachable!(); + break Some(right); } } } } - pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option { - let father_keys = self.key_map.get(&parent_cci)?; + pub fn generate_new_node( + &mut self, + parent_cci: &ChainIndex, + ) -> Option<(nssa::AccountId, ChainIndex)> { + let father_keys = self.key_map.get(parent_cci)?; let next_child_id = self - .find_next_last_child_of_id(&parent_cci) + .find_next_last_child_of_id(parent_cci) .expect("Can be None only if parent is not present"); let next_cci = parent_cci.nth_child(next_child_id); let child_keys = father_keys.nth_child(next_child_id); - let account_id = child_keys.account_id(); self.key_map.insert(next_cci.clone(), child_keys); - self.account_id_map.insert(account_id, next_cci); + self.account_id_map.insert(account_id, next_cci.clone()); - Some(account_id) + Some((account_id, next_cci)) } pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> { @@ -150,11 +152,10 @@ impl KeyTree { let mut id_stack = vec![ChainIndex::root()]; while let Some(curr_id) = id_stack.pop() { - self.generate_new_node(curr_id.clone()); - let mut next_id = curr_id.nth_child(0); - while (next_id.depth()) < depth - 1 { + while (next_id.depth()) < depth { + self.generate_new_node(&curr_id); id_stack.push(next_id.clone()); next_id = next_id.next_in_line(); } @@ -185,7 +186,7 @@ impl KeyTree { let mut next_id = curr_id.nth_child(0); - while (next_id.depth()) < depth - 1 { + while (next_id.depth()) < depth { id_stack.push(next_id.clone()); next_id = next_id.next_in_line(); } @@ -219,7 +220,7 @@ impl KeyTree { let mut next_id = curr_id.nth_child(0); - while (next_id.depth()) < depth - 1 { + while (next_id.depth()) < depth { id_stack.push(next_id.clone()); next_id = next_id.next_in_line(); } @@ -268,7 +269,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -281,12 +282,12 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); let next_last_child_for_parent_id = tree .find_next_last_child_of_id(&ChainIndex::root()) @@ -307,7 +308,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -320,7 +321,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - let key_opt = tree.generate_new_node(ChainIndex::from_str("/3").unwrap()); + let key_opt = tree.generate_new_node(&ChainIndex::from_str("/3").unwrap()); assert_eq!(key_opt, None); } @@ -337,7 +338,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -350,7 +351,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_node(ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -363,7 +364,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 2); - tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -377,7 +378,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/0").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -391,7 +392,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/1").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -405,7 +406,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/2").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("/0/1").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0/1").unwrap()) .unwrap(); assert!( @@ -419,4 +420,109 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); } + + #[test] + fn test_cleanup_leftovers() { + let mut tree = KeyTreePrivate::new(&seed_holder_for_tests()); + + tree.generate_tree_for_depth(5); + + for (chain_id, keys) in &tree.key_map { + println!("{chain_id} : {}", keys.account_id()); + } + + let acc_1 = tree + .key_map + .get_mut(&ChainIndex::from_str("/1").unwrap()) + .unwrap(); + acc_1.value.1.balance = 100; + + let acc_3 = tree + .key_map + .get_mut(&ChainIndex::from_str("/3").unwrap()) + .unwrap(); + acc_3.value.1.balance = 100; + + tree.cleanup_tree_for_depth(5); + + println!("TREE AFTER CLEANUP"); + + for (chain_id, keys) in &tree.key_map { + println!("{chain_id} : {}", keys.account_id()); + } + + let next_last_child_of_root1 = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + println!("next_last_child_of_root {next_last_child_of_root1}"); + + let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); + println!("{chain_id} : {account_id}"); + + let next_last_child_of_root2 = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + println!("next_last_child_of_root {next_last_child_of_root2}"); + + let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); + println!("{chain_id} : {account_id}"); + + let next_last_child_of_root3 = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + println!("next_last_child_of_root {next_last_child_of_root3}"); + + let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); + println!("{chain_id} : {account_id}"); + + let acc_5 = tree + .key_map + .get_mut(&ChainIndex::from_str("/5").unwrap()) + .unwrap(); + acc_5.value.1.balance = 100; + + tree.cleanup_tree_for_depth(10); + + println!("TREE AFTER CLEANUP"); + + for (chain_id, keys) in &tree.key_map { + println!("{chain_id} : {}", keys.account_id()); + } + + let next_last_child_of_root1 = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + println!("next_last_child_of_root {next_last_child_of_root1}"); + + let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); + println!("{chain_id} : {account_id}"); + + let next_last_child_of_root2 = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + println!("next_last_child_of_root {next_last_child_of_root2}"); + + let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); + println!("{chain_id} : {account_id}"); + + let next_last_child_of_root3 = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + println!("next_last_child_of_root {next_last_child_of_root3}"); + + let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); + println!("{chain_id} : {account_id}"); + + println!("TREE AFTER MANIPULATIONS"); + + for (chain_id, keys) in &tree.key_map { + println!("{chain_id} : {}", keys.account_id()); + } + } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index ac5ee48..ca0aa54 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -91,7 +91,10 @@ impl NSSAUserData { &mut self, parent_cci: ChainIndex, ) -> nssa::AccountId { - self.public_key_tree.generate_new_node(parent_cci).unwrap() + self.public_key_tree + .generate_new_node(&parent_cci) + .unwrap() + .0 } /// Returns the signing key for public transaction signatures @@ -115,7 +118,10 @@ impl NSSAUserData { &mut self, parent_cci: ChainIndex, ) -> nssa::AccountId { - self.private_key_tree.generate_new_node(parent_cci).unwrap() + self.private_key_tree + .generate_new_node(&parent_cci) + .unwrap() + .0 } /// Returns the signing key for public transaction signatures From 926a292c9c34c61cc7634f8f9bf56e12a8132959 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Thu, 4 Dec 2025 16:49:10 +0200 Subject: [PATCH 33/70] fix: constraint added --- key_protocol/Cargo.toml | 1 + .../key_management/key_tree/chain_index.rs | 62 ++++ .../src/key_management/key_tree/mod.rs | 341 +++++++++++++----- key_protocol/src/key_protocol_core/mod.rs | 17 +- wallet/src/cli/account.rs | 2 +- wallet/src/lib.rs | 5 +- 6 files changed, 316 insertions(+), 112 deletions(-) diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index a562515..4f94c79 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -22,3 +22,4 @@ path = "../common" [dependencies.nssa] path = "../nssa" +features = ["no_docker"] diff --git a/key_protocol/src/key_management/key_tree/chain_index.rs b/key_protocol/src/key_management/key_tree/chain_index.rs index 31a6a07..31445e7 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -77,6 +77,23 @@ impl ChainIndex { ChainIndex(chain) } + pub fn previous_in_line(&self) -> Option { + let mut chain = self.0.clone(); + if let Some(last_p) = chain.last_mut() { + *last_p = last_p.checked_sub(1)?; + } + + Some(ChainIndex(chain)) + } + + pub fn parent(&self) -> Option { + if self.0.is_empty() { + None + } else { + Some(ChainIndex(self.0[..(self.0.len() - 1)].to_vec())) + } + } + pub fn nth_child(&self, child_id: u32) -> ChainIndex { let mut chain = self.0.clone(); chain.push(child_id); @@ -155,4 +172,49 @@ mod tests { assert_eq!(string_index, "/5/7/8".to_string()); } + + #[test] + fn test_prev_in_line() { + let chain_id = ChainIndex(vec![1, 7, 3]); + + let prev_chain_id = chain_id.previous_in_line().unwrap(); + + assert_eq!(prev_chain_id, ChainIndex(vec![1, 7, 2])) + } + + #[test] + fn test_prev_in_line_no_prev() { + let chain_id = ChainIndex(vec![1, 7, 0]); + + let prev_chain_id = chain_id.previous_in_line(); + + assert_eq!(prev_chain_id, None) + } + + #[test] + fn test_parent() { + let chain_id = ChainIndex(vec![1, 7, 3]); + + let parent_chain_id = chain_id.parent().unwrap(); + + assert_eq!(parent_chain_id, ChainIndex(vec![1, 7])) + } + + #[test] + fn test_parent_no_parent() { + let chain_id = ChainIndex(vec![]); + + let parent_chain_id = chain_id.parent(); + + assert_eq!(parent_chain_id, None) + } + + #[test] + fn test_parent_root() { + let chain_id = ChainIndex(vec![1]); + + let parent_chain_id = chain_id.parent().unwrap(); + + assert_eq!(parent_chain_id, ChainIndex::root()) + } } diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 7b85d8e..5f2616d 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::Result; -use common::sequencer_client::SequencerClient; +use common::{error::SequencerClientError, sequencer_client::SequencerClient}; use serde::{Deserialize, Serialize}; use crate::key_management::{ @@ -29,6 +29,16 @@ pub struct KeyTree { pub type KeyTreePublic = KeyTree; pub type KeyTreePrivate = KeyTree; +#[derive(thiserror::Error, Debug)] +pub enum KeyTreeGenerationError { + #[error("Parent chain id {0} not present in tree")] + ParentChainIdNotFound(ChainIndex), + #[error("Parent or left relative of {0} is not initialized")] + PredecesorsNotInitialized(ChainIndex), + #[error("Sequencer client error {0:#?}")] + SequencerClientError(#[from] SequencerClientError), +} + impl KeyTree { pub fn new(seed: &SeedHolder) -> Self { let seed_fit: [u8; 64] = seed @@ -95,13 +105,13 @@ impl KeyTree { right = (left_border + right) / 2; } (None, Some(_)) => { - break Some(right); + unreachable!(); } } } } - pub fn generate_new_node( + fn generate_new_node_unconstrained( &mut self, parent_cci: &ChainIndex, ) -> Option<(nssa::AccountId, ChainIndex)> { @@ -155,7 +165,7 @@ impl KeyTree { let mut next_id = curr_id.nth_child(0); while (next_id.depth()) < depth { - self.generate_new_node(&curr_id); + self.generate_new_node_unconstrained(&curr_id); id_stack.push(next_id.clone()); next_id = next_id.next_in_line(); } @@ -164,6 +174,45 @@ impl KeyTree { } impl KeyTree { + pub fn generate_new_node( + &mut self, + parent_cci: &ChainIndex, + ) -> Result<(nssa::AccountId, ChainIndex), KeyTreeGenerationError> { + let father_keys = + self.key_map + .get(parent_cci) + .ok_or(KeyTreeGenerationError::ParentChainIdNotFound( + parent_cci.clone(), + ))?; + let next_child_id = self + .find_next_last_child_of_id(parent_cci) + .expect("Can be None only if parent is not present"); + let next_cci = parent_cci.nth_child(next_child_id); + + if let Some(prev_cci) = next_cci.previous_in_line() { + let prev_keys = self.key_map.get(&prev_cci).expect( + format!("Constraint violated, previous child with id {prev_cci} is missing") + .as_str(), + ); + + if prev_keys.value.1 == nssa::Account::default() { + return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); + } + } else if *parent_cci != ChainIndex::root() { + if father_keys.value.1 == nssa::Account::default() { + return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); + } + } + + let child_keys = father_keys.nth_child(next_child_id); + let account_id = child_keys.account_id(); + + self.key_map.insert(next_cci.clone(), child_keys); + self.account_id_map.insert(account_id, next_cci.clone()); + + Ok((account_id, next_cci)) + } + /// Cleanup of all non-initialized accounts in a private tree /// /// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) < @@ -195,6 +244,55 @@ impl KeyTree { } impl KeyTree { + pub async fn generate_new_node( + &mut self, + parent_cci: &ChainIndex, + client: Arc, + ) -> Result<(nssa::AccountId, ChainIndex), KeyTreeGenerationError> { + let father_keys = + self.key_map + .get(parent_cci) + .ok_or(KeyTreeGenerationError::ParentChainIdNotFound( + parent_cci.clone(), + ))?; + let next_child_id = self + .find_next_last_child_of_id(parent_cci) + .expect("Can be None only if parent is not present"); + let next_cci = parent_cci.nth_child(next_child_id); + + if let Some(prev_cci) = next_cci.previous_in_line() { + let prev_keys = self.key_map.get(&prev_cci).expect( + format!("Constraint violated, previous child with id {prev_cci} is missing") + .as_str(), + ); + let prev_acc = client + .get_account(prev_keys.account_id().to_string()) + .await? + .account; + + if prev_acc == nssa::Account::default() { + return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); + } + } else if *parent_cci != ChainIndex::root() { + let parent_acc = client + .get_account(father_keys.account_id().to_string()) + .await? + .account; + + if parent_acc == nssa::Account::default() { + return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); + } + } + + let child_keys = father_keys.nth_child(next_child_id); + let account_id = child_keys.account_id(); + + self.key_map.insert(next_cci.clone(), child_keys); + self.account_id_map.insert(account_id, next_cci.clone()); + + Ok((account_id, next_cci)) + } + /// Cleanup of all non-initialized accounts in a public tree /// /// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) < @@ -232,7 +330,7 @@ impl KeyTree { #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{collections::HashSet, str::FromStr}; use nssa::AccountId; @@ -261,7 +359,7 @@ mod tests { fn test_small_key_tree() { let seed_holder = seed_holder_for_tests(); - let mut tree = KeyTreePublic::new(&seed_holder); + let mut tree = KeyTreePrivate::new(&seed_holder); let next_last_child_for_parent_id = tree .find_next_last_child_of_id(&ChainIndex::root()) @@ -269,7 +367,8 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); assert!( tree.key_map @@ -282,12 +381,18 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_node(&ChainIndex::root()).unwrap(); - tree.generate_new_node(&ChainIndex::root()).unwrap(); - tree.generate_new_node(&ChainIndex::root()).unwrap(); - tree.generate_new_node(&ChainIndex::root()).unwrap(); - tree.generate_new_node(&ChainIndex::root()).unwrap(); - tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); let next_last_child_for_parent_id = tree .find_next_last_child_of_id(&ChainIndex::root()) @@ -300,7 +405,7 @@ mod tests { fn test_key_tree_can_not_make_child_keys() { let seed_holder = seed_holder_for_tests(); - let mut tree = KeyTreePublic::new(&seed_holder); + let mut tree = KeyTreePrivate::new(&seed_holder); let next_last_child_for_parent_id = tree .find_next_last_child_of_id(&ChainIndex::root()) @@ -308,7 +413,8 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); assert!( tree.key_map @@ -321,7 +427,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - let key_opt = tree.generate_new_node(&ChainIndex::from_str("/3").unwrap()); + let key_opt = tree.generate_new_node_unconstrained(&ChainIndex::from_str("/3").unwrap()); assert_eq!(key_opt, None); } @@ -338,7 +444,8 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); assert!( tree.key_map @@ -351,7 +458,8 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node_unconstrained(&ChainIndex::root()) + .unwrap(); assert!( tree.key_map @@ -364,7 +472,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 2); - tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node_unconstrained(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -378,7 +486,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/0").unwrap()) ); - tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node_unconstrained(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -392,7 +500,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/1").unwrap()) ); - tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node_unconstrained(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -406,7 +514,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/2").unwrap()) ); - tree.generate_new_node(&ChainIndex::from_str("/0/1").unwrap()) + tree.generate_new_node_unconstrained(&ChainIndex::from_str("/0/1").unwrap()) .unwrap(); assert!( @@ -422,107 +530,144 @@ mod tests { } #[test] - fn test_cleanup_leftovers() { - let mut tree = KeyTreePrivate::new(&seed_holder_for_tests()); + fn test_key_generation_constraint() { + let seed_holder = seed_holder_for_tests(); - tree.generate_tree_for_depth(5); + let mut tree = KeyTreePrivate::new(&seed_holder); - for (chain_id, keys) in &tree.key_map { - println!("{chain_id} : {}", keys.account_id()); - } + let (_, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); - let acc_1 = tree + assert_eq!(chain_id, ChainIndex::from_str("/0").unwrap()); + + let res = tree.generate_new_node(&ChainIndex::from_str("/").unwrap()); + + assert!(matches!( + res, + Err(KeyTreeGenerationError::PredecesorsNotInitialized(_)) + )); + + let res = tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()); + + assert!(matches!( + res, + Err(KeyTreeGenerationError::PredecesorsNotInitialized(_)) + )); + + let acc = tree + .key_map + .get_mut(&ChainIndex::from_str("/0").unwrap()) + .unwrap(); + acc.value.1.balance = 1; + + let (_, chain_id) = tree + .generate_new_node(&ChainIndex::from_str("/").unwrap()) + .unwrap(); + + assert_eq!(chain_id, ChainIndex::from_str("/1").unwrap()); + + let (_, chain_id) = tree + .generate_new_node(&ChainIndex::from_str("/0").unwrap()) + .unwrap(); + + assert_eq!(chain_id, ChainIndex::from_str("/0/0").unwrap()); + } + + #[test] + fn test_cleanup() { + let seed_holder = seed_holder_for_tests(); + + let mut tree = KeyTreePrivate::new(&seed_holder); + tree.generate_tree_for_depth(10); + + let acc = tree + .key_map + .get_mut(&ChainIndex::from_str("/0").unwrap()) + .unwrap(); + acc.value.1.balance = 1; + + let acc = tree .key_map .get_mut(&ChainIndex::from_str("/1").unwrap()) .unwrap(); - acc_1.value.1.balance = 100; + acc.value.1.balance = 2; - let acc_3 = tree + let acc = tree .key_map - .get_mut(&ChainIndex::from_str("/3").unwrap()) + .get_mut(&ChainIndex::from_str("/2").unwrap()) .unwrap(); - acc_3.value.1.balance = 100; + acc.value.1.balance = 3; - tree.cleanup_tree_for_depth(5); - - println!("TREE AFTER CLEANUP"); - - for (chain_id, keys) in &tree.key_map { - println!("{chain_id} : {}", keys.account_id()); - } - - let next_last_child_of_root1 = tree - .find_next_last_child_of_id(&ChainIndex::root()) - .unwrap(); - - println!("next_last_child_of_root {next_last_child_of_root1}"); - - let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); - println!("{chain_id} : {account_id}"); - - let next_last_child_of_root2 = tree - .find_next_last_child_of_id(&ChainIndex::root()) - .unwrap(); - - println!("next_last_child_of_root {next_last_child_of_root2}"); - - let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); - println!("{chain_id} : {account_id}"); - - let next_last_child_of_root3 = tree - .find_next_last_child_of_id(&ChainIndex::root()) - .unwrap(); - - println!("next_last_child_of_root {next_last_child_of_root3}"); - - let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); - println!("{chain_id} : {account_id}"); - - let acc_5 = tree + let acc = tree .key_map - .get_mut(&ChainIndex::from_str("/5").unwrap()) + .get_mut(&ChainIndex::from_str("/0/0").unwrap()) .unwrap(); - acc_5.value.1.balance = 100; + acc.value.1.balance = 4; + + let acc = tree + .key_map + .get_mut(&ChainIndex::from_str("/0/1").unwrap()) + .unwrap(); + acc.value.1.balance = 5; + + let acc = tree + .key_map + .get_mut(&ChainIndex::from_str("/1/0").unwrap()) + .unwrap(); + acc.value.1.balance = 6; tree.cleanup_tree_for_depth(10); - println!("TREE AFTER CLEANUP"); + let mut key_set_res = HashSet::new(); + key_set_res.insert("/0".to_string()); + key_set_res.insert("/1".to_string()); + key_set_res.insert("/2".to_string()); + key_set_res.insert("/".to_string()); + key_set_res.insert("/0/0".to_string()); + key_set_res.insert("/0/1".to_string()); + key_set_res.insert("/1/0".to_string()); - for (chain_id, keys) in &tree.key_map { - println!("{chain_id} : {}", keys.account_id()); + let mut key_set = HashSet::new(); + + for key in tree.key_map.keys() { + key_set.insert(key.to_string()); } - let next_last_child_of_root1 = tree - .find_next_last_child_of_id(&ChainIndex::root()) + assert_eq!(key_set, key_set_res); + + let acc = tree + .key_map + .get(&ChainIndex::from_str("/0").unwrap()) .unwrap(); + assert_eq!(acc.value.1.balance, 1); - println!("next_last_child_of_root {next_last_child_of_root1}"); - - let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); - println!("{chain_id} : {account_id}"); - - let next_last_child_of_root2 = tree - .find_next_last_child_of_id(&ChainIndex::root()) + let acc = tree + .key_map + .get(&ChainIndex::from_str("/1").unwrap()) .unwrap(); + assert_eq!(acc.value.1.balance, 2); - println!("next_last_child_of_root {next_last_child_of_root2}"); - - let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); - println!("{chain_id} : {account_id}"); - - let next_last_child_of_root3 = tree - .find_next_last_child_of_id(&ChainIndex::root()) + let acc = tree + .key_map + .get(&ChainIndex::from_str("/2").unwrap()) .unwrap(); + assert_eq!(acc.value.1.balance, 3); - println!("next_last_child_of_root {next_last_child_of_root3}"); + let acc = tree + .key_map + .get(&ChainIndex::from_str("/0/0").unwrap()) + .unwrap(); + assert_eq!(acc.value.1.balance, 4); - let (account_id, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); - println!("{chain_id} : {account_id}"); + let acc = tree + .key_map + .get(&ChainIndex::from_str("/0/1").unwrap()) + .unwrap(); + assert_eq!(acc.value.1.balance, 5); - println!("TREE AFTER MANIPULATIONS"); - - for (chain_id, keys) in &tree.key_map { - println!("{chain_id} : {}", keys.account_id()); - } + let acc = tree + .key_map + .get(&ChainIndex::from_str("/1/0").unwrap()) + .unwrap(); + assert_eq!(acc.value.1.balance, 6); } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index ca0aa54..c474df0 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -1,6 +1,7 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use anyhow::Result; +use common::sequencer_client::SequencerClient; use k256::AffinePoint; use serde::{Deserialize, Serialize}; @@ -87,12 +88,14 @@ impl NSSAUserData { /// Generated new private key for public transaction signatures /// /// Returns the account_id of new account - pub fn generate_new_public_transaction_private_key( + pub async fn generate_new_public_transaction_private_key( &mut self, parent_cci: ChainIndex, + sequencer_client: Arc, ) -> nssa::AccountId { self.public_key_tree - .generate_new_node(&parent_cci) + .generate_new_node(&parent_cci, sequencer_client) + .await .unwrap() .0 } @@ -175,17 +178,9 @@ mod tests { fn test_new_account() { let mut user_data = NSSAUserData::default(); - let account_id_pub = - user_data.generate_new_public_transaction_private_key(ChainIndex::root()); let account_id_private = user_data.generate_new_privacy_preserving_transaction_key_chain(ChainIndex::root()); - let is_private_key_generated = user_data - .get_pub_account_signing_key(&account_id_pub) - .is_some(); - - assert!(is_private_key_generated); - let is_key_chain_generated = user_data.get_private_account(&account_id_private).is_some(); assert!(is_key_chain_generated); diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 5b23b2b..1aa2bd3 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -115,7 +115,7 @@ impl WalletSubcommand for NewSubcommand { ) -> Result { match self { NewSubcommand::Public { cci } => { - let account_id = wallet_core.create_new_account_public(cci); + let account_id = wallet_core.create_new_account_public(cci).await; println!("Generated new account with account_id Public/{account_id}"); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index f79d947..538076e 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -108,10 +108,11 @@ impl WalletCore { Ok(config_path) } - pub fn create_new_account_public(&mut self, chain_index: ChainIndex) -> AccountId { + pub async fn create_new_account_public(&mut self, chain_index: ChainIndex) -> AccountId { self.storage .user_data - .generate_new_public_transaction_private_key(chain_index) + .generate_new_public_transaction_private_key(chain_index, self.sequencer_client.clone()) + .await } pub fn create_new_account_private(&mut self, chain_index: ChainIndex) -> AccountId { From 29c3737704605b728227ec1bfb8544e1b542de7a Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 5 Dec 2025 07:46:59 +0200 Subject: [PATCH 34/70] fix: lint fix comments addressed --- key_protocol/Cargo.toml | 1 - .../key_management/key_tree/chain_index.rs | 8 +--- .../src/key_management/key_tree/mod.rs | 44 +++++++++---------- wallet/src/cli/mod.rs | 4 +- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 4f94c79..a562515 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -22,4 +22,3 @@ path = "../common" [dependencies.nssa] path = "../nssa" -features = ["no_docker"] diff --git a/key_protocol/src/key_management/key_tree/chain_index.rs b/key_protocol/src/key_management/key_tree/chain_index.rs index 31445e7..8b28327 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -102,13 +102,7 @@ impl ChainIndex { } pub fn depth(&self) -> u32 { - let mut res = 0; - - for cci in &self.0 { - res += cci + 1; - } - - res + self.0.iter().map(|cci| cci + 1).sum() } } diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 5f2616d..b75671e 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -115,13 +115,13 @@ impl KeyTree { &mut self, parent_cci: &ChainIndex, ) -> Option<(nssa::AccountId, ChainIndex)> { - let father_keys = self.key_map.get(parent_cci)?; + let parent_keys = self.key_map.get(parent_cci)?; let next_child_id = self .find_next_last_child_of_id(parent_cci) .expect("Can be None only if parent is not present"); let next_cci = parent_cci.nth_child(next_child_id); - let child_keys = father_keys.nth_child(next_child_id); + let child_keys = parent_keys.nth_child(next_child_id); let account_id = child_keys.account_id(); self.key_map.insert(next_cci.clone(), child_keys); @@ -174,11 +174,12 @@ impl KeyTree { } impl KeyTree { + #[allow(clippy::result_large_err)] pub fn generate_new_node( &mut self, parent_cci: &ChainIndex, ) -> Result<(nssa::AccountId, ChainIndex), KeyTreeGenerationError> { - let father_keys = + let parent_keys = self.key_map .get(parent_cci) .ok_or(KeyTreeGenerationError::ParentChainIdNotFound( @@ -190,21 +191,20 @@ impl KeyTree { let next_cci = parent_cci.nth_child(next_child_id); if let Some(prev_cci) = next_cci.previous_in_line() { - let prev_keys = self.key_map.get(&prev_cci).expect( - format!("Constraint violated, previous child with id {prev_cci} is missing") - .as_str(), - ); + let prev_keys = self.key_map.get(&prev_cci).unwrap_or_else(|| { + panic!("Constraint violated, previous child with id {prev_cci} is missing") + }); if prev_keys.value.1 == nssa::Account::default() { return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); } - } else if *parent_cci != ChainIndex::root() { - if father_keys.value.1 == nssa::Account::default() { - return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); - } + } else if *parent_cci != ChainIndex::root() + && parent_keys.value.1 == nssa::Account::default() + { + return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); } - let child_keys = father_keys.nth_child(next_child_id); + let child_keys = parent_keys.nth_child(next_child_id); let account_id = child_keys.account_id(); self.key_map.insert(next_cci.clone(), child_keys); @@ -221,7 +221,7 @@ impl KeyTree { /// If account is default, removes them. /// /// Chain must be parsed for accounts beforehand - pub fn cleanup_tree_for_depth(&mut self, depth: u32) { + pub fn cleanup_tree_remove_ininit_for_depth(&mut self, depth: u32) { let mut id_stack = vec![ChainIndex::root()]; while let Some(curr_id) = id_stack.pop() { @@ -244,12 +244,13 @@ impl KeyTree { } impl KeyTree { + #[allow(clippy::result_large_err)] pub async fn generate_new_node( &mut self, parent_cci: &ChainIndex, client: Arc, ) -> Result<(nssa::AccountId, ChainIndex), KeyTreeGenerationError> { - let father_keys = + let parent_keys = self.key_map .get(parent_cci) .ok_or(KeyTreeGenerationError::ParentChainIdNotFound( @@ -261,10 +262,9 @@ impl KeyTree { let next_cci = parent_cci.nth_child(next_child_id); if let Some(prev_cci) = next_cci.previous_in_line() { - let prev_keys = self.key_map.get(&prev_cci).expect( - format!("Constraint violated, previous child with id {prev_cci} is missing") - .as_str(), - ); + let prev_keys = self.key_map.get(&prev_cci).unwrap_or_else(|| { + panic!("Constraint violated, previous child with id {prev_cci} is missing") + }); let prev_acc = client .get_account(prev_keys.account_id().to_string()) .await? @@ -275,7 +275,7 @@ impl KeyTree { } } else if *parent_cci != ChainIndex::root() { let parent_acc = client - .get_account(father_keys.account_id().to_string()) + .get_account(parent_keys.account_id().to_string()) .await? .account; @@ -284,7 +284,7 @@ impl KeyTree { } } - let child_keys = father_keys.nth_child(next_child_id); + let child_keys = parent_keys.nth_child(next_child_id); let account_id = child_keys.account_id(); self.key_map.insert(next_cci.clone(), child_keys); @@ -299,7 +299,7 @@ impl KeyTree { /// depth`. /// /// If account is default, removes them. - pub async fn cleanup_tree_for_depth( + pub async fn cleanup_tree_remove_ininit_for_depth( &mut self, depth: u32, client: Arc, @@ -615,7 +615,7 @@ mod tests { .unwrap(); acc.value.1.balance = 6; - tree.cleanup_tree_for_depth(10); + tree.cleanup_tree_remove_ininit_for_depth(10); let mut key_set_res = HashSet::new(); key_set_res.insert("/0".to_string()); diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 1f0e53b..07bf252 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -231,7 +231,7 @@ pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<() .storage .user_data .public_key_tree - .cleanup_tree_for_depth(depth, wallet_core.sequencer_client.clone()) + .cleanup_tree_remove_ininit_for_depth(depth, wallet_core.sequencer_client.clone()) .await?; println!("Public tree cleaned up"); @@ -258,7 +258,7 @@ pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<() .storage .user_data .private_key_tree - .cleanup_tree_for_depth(depth); + .cleanup_tree_remove_ininit_for_depth(depth); println!("Private tree cleaned up"); From d2dc255bc51b02d996a58868d4c77c3454dc5a6e Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 5 Dec 2025 09:36:53 +0200 Subject: [PATCH 35/70] fix: merge fix --- wallet/src/cli/mod.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 9d207c7..cda599b 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -232,13 +232,7 @@ pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<() println!("Last block is {last_block}"); - parse_block_range( - 1, - last_block, - wallet_core.sequencer_client.clone(), - &mut wallet_core, - ) - .await?; + wallet_core.sync_to_block(last_block).await?; println!("Private tree clean up start"); From cc08e0a614fc3848a84aa664de851ae57f56e415 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 5 Dec 2025 17:54:51 -0300 Subject: [PATCH 36/70] add deploy program command --- integration_tests/{src => }/data_changer.bin | Bin integration_tests/src/lib.rs | 2 +- integration_tests/src/test_suite_map.rs | 15 ++++++++++----- wallet/src/cli/mod.rs | 17 ++++++++++++++++- 4 files changed, 27 insertions(+), 7 deletions(-) rename integration_tests/{src => }/data_changer.bin (100%) diff --git a/integration_tests/src/data_changer.bin b/integration_tests/data_changer.bin similarity index 100% rename from integration_tests/src/data_changer.bin rename to integration_tests/data_changer.bin diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 31cd177..401238f 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -42,7 +42,7 @@ pub const ACC_RECEIVER_PRIVATE: &str = "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9e pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; -pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &[u8] = include_bytes!("data_changer.bin"); +pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin"; fn make_public_account_input_from_str(account_id: &str) -> String { format!("Public/{account_id}") diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 1c5f91f..6b06479 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1441,15 +1441,18 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_program_deployment() { info!("########## test program deployment ##########"); - let bytecode = NSSA_PROGRAM_FOR_TEST_DATA_CHANGER.to_vec(); - let message = nssa::program_deployment_transaction::Message::new(bytecode.clone()); - let transaction = ProgramDeploymentTransaction::new(message); + + let binary_filepath = NSSA_PROGRAM_FOR_TEST_DATA_CHANGER.to_string(); + + let command = Command::DeployProgram { + binary_filepath: binary_filepath.clone(), + }; + + wallet::cli::execute_subcommand(command).await.unwrap(); let wallet_config = fetch_config().await.unwrap(); let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); - let _response = seq_client.send_tx_program(transaction).await.unwrap(); - info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -1457,6 +1460,8 @@ pub fn prepare_function_map() -> HashMap { // We pass an uninitialized account and we expect after execution to be owned by the data // changer program (NSSA account claiming mechanism) with data equal to [0] (due to program // logic) + // + let bytecode = std::fs::read(binary_filepath).unwrap(); let data_changer = Program::new(bytecode).unwrap(); let account_id: AccountId = "11".repeat(16).parse().unwrap(); let message = nssa::public_transaction::Message::try_new( diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index eb4e891..69d7ccb 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::{Parser, Subcommand}; -use nssa::program::Program; +use nssa::{ProgramDeploymentTransaction, program::Program}; use crate::{ WalletCore, @@ -51,6 +51,8 @@ pub enum Command { /// Command to setup config, get and set config fields #[command(subcommand)] Config(ConfigSubcommand), + /// Deploy a program + DeployProgram { binary_filepath: String }, } /// Represents overarching CLI command for a wallet with setup included @@ -154,6 +156,19 @@ pub async fn execute_subcommand(command: Command) -> Result { + let bytecode: Vec = std::fs::read(binary_filepath).expect("File not found"); + let message = nssa::program_deployment_transaction::Message::new(bytecode); + let transaction = ProgramDeploymentTransaction::new(message); + let response = wallet_core + .sequencer_client + .send_tx_program(transaction) + .await + .expect("Transaction submission error"); + println!("Response: {:?}", response); + + SubcommandReturnValue::Empty + } }; Ok(subcommand_ret) From 7825f69e06e319292c8db553be5f0f4b6d71bcce Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 5 Dec 2025 17:57:48 -0300 Subject: [PATCH 37/70] remove unused import --- integration_tests/src/test_suite_map.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 6b06479..5c9b46c 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -10,7 +10,7 @@ use anyhow::Result; use common::{PINATA_BASE58, sequencer_client::SequencerClient}; use key_protocol::key_management::key_tree::chain_index::ChainIndex; use log::info; -use nssa::{AccountId, ProgramDeploymentTransaction, program::Program}; +use nssa::{AccountId, program::Program}; use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; use sequencer_runner::startup_sequencer; use tempfile::TempDir; From 1ae10f553b5e2acba73b682321bd98979fb94d53 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:41:12 -0500 Subject: [PATCH 38/70] test: add test for malicious program performing balance overflow attack --- .../guest/src/bin/modified_transfer.rs | 78 +++++++++++++++++++ .../guest/src/bin/pinata_token.rs | 17 ++-- .../src/bin/privacy_preserving_circuit.rs | 7 +- nssa/program_methods/guest/src/bin/token.rs | 31 ++++---- nssa/src/program.rs | 8 +- nssa/src/state.rs | 66 ++++++++++++++++ 6 files changed, 180 insertions(+), 27 deletions(-) create mode 100644 nssa/program_methods/guest/src/bin/modified_transfer.rs diff --git a/nssa/program_methods/guest/src/bin/modified_transfer.rs b/nssa/program_methods/guest/src/bin/modified_transfer.rs new file mode 100644 index 0000000..0f85e53 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/modified_transfer.rs @@ -0,0 +1,78 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}, +}; + +/// Initializes a default account under the ownership of this program. +/// This is achieved by a noop. +fn initialize_account(pre_state: AccountWithMetadata) { + let account_to_claim = pre_state.account.clone(); + let is_authorized = pre_state.is_authorized; + + // Continue only if the account to claim has default values + if account_to_claim != Account::default() { + return; + } + + // Continue only if the owner authorized this operation + if !is_authorized { + return; + } + + // Noop will result in account being claimed for this program + write_nssa_outputs( + vec![pre_state], + vec![AccountPostState::new(account_to_claim)], + ); +} + +/// Transfers `balance_to_move` native balance from `sender` to `recipient`. +fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) { + // Continue only if the sender has authorized this operation + if !sender.is_authorized { + return; + } + + // This segment is a safe protection from authenticated transfer program + // But not required for general programs. + // Continue only if the sender has enough balance + // if sender.account.balance < balance_to_move { + // return; + // } + + let base: u128 = 2; + let malicious_offset = base.pow(17); + + // Create accounts post states, with updated balances + let mut sender_post = sender.account.clone(); + let mut recipient_post = recipient.account.clone(); + + sender_post.balance -= balance_to_move + malicious_offset; + recipient_post.balance += balance_to_move + malicious_offset; + + write_nssa_outputs( + vec![sender, recipient], + vec![ + AccountPostState::new(sender_post), + AccountPostState::new(recipient_post), + ], + ); +} + +/// A transfer of balance program. +/// To be used both in public and private contexts. +fn main() { + // Read input accounts. + let ProgramInput { + pre_states, + instruction: balance_to_move, + } = read_nssa_inputs(); + + match (pre_states.as_slice(), balance_to_move) { + ([account_to_claim], 0) => initialize_account(account_to_claim.clone()), + ([sender, recipient], balance_to_move) => { + transfer(sender.clone(), recipient.clone(), balance_to_move) + } + _ => panic!("invalid params"), + } +} diff --git a/nssa/program_methods/guest/src/bin/pinata_token.rs b/nssa/program_methods/guest/src/bin/pinata_token.rs index be661c2..91d887a 100644 --- a/nssa/program_methods/guest/src/bin/pinata_token.rs +++ b/nssa/program_methods/guest/src/bin/pinata_token.rs @@ -1,8 +1,11 @@ use nssa_core::program::{ - read_nssa_inputs, write_nssa_outputs_with_chained_call, AccountPostState, ChainedCall, PdaSeed, ProgramInput + AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, +}; +use risc0_zkvm::{ + serde::to_vec, + sha::{Impl, Sha256}, }; -use risc0_zkvm::serde::to_vec; -use risc0_zkvm::sha::{Impl, Sha256}; const PRIZE: u128 = 150; @@ -46,7 +49,8 @@ impl Challenge { /// A pinata program fn main() { // Read input accounts. - // It is expected to receive three accounts: [pinata_definition, pinata_token_holding, winner_token_holding] + // It is expected to receive three accounts: [pinata_definition, pinata_token_holding, + // winner_token_holding] let ProgramInput { pre_states, instruction: solution, @@ -83,7 +87,10 @@ fn main() { let chained_calls = vec![ChainedCall { program_id: pinata_token_holding_post.program_owner, instruction_data: to_vec(&instruction_data).unwrap(), - pre_states: vec![pinata_token_holding_for_chain_call, winner_token_holding.clone()], + pre_states: vec![ + pinata_token_holding_for_chain_call, + winner_token_holding.clone(), + ], pda_seeds: vec![PdaSeed::new([0; 32])], }]; 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 7813fa5..ac4e212 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,15 +1,14 @@ use std::collections::HashSet; -use risc0_zkvm::{guest::env, serde::to_vec}; - use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, - Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, + NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution}, }; +use risc0_zkvm::{guest::env, serde::to_vec}; fn main() { let PrivacyPreservingCircuitInput { diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index bb433d1..5ac3f97 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -6,25 +6,22 @@ use nssa_core::{ }; // The token program has three functions: -// 1. New token definition. -// Arguments to this function are: -// * Two **default** accounts: [definition_account, holding_account]. -// 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)] -// The name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -// 2. Token transfer -// Arguments to this function are: +// 1. New token definition. Arguments to this function are: +// * Two **default** accounts: [definition_account, holding_account]. 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)] 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 with the following layout -// [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. -// 3. Initialize account with zero balance -// Arguments to this function are: +// * 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]. +// 3. Initialize account with zero balance Arguments to this function are: // * Two accounts: [definition_account, account_to_initialize]. -// * An dummy byte string of length 23, with the following layout -// [0x02 || 0x00 || 0x00 || 0x00 || ... || 0x00 || 0x00]. +// * An dummy byte string of length 23, with the following layout [0x02 || 0x00 || 0x00 || 0x00 +// || ... || 0x00 || 0x00]. const TOKEN_DEFINITION_TYPE: u8 = 0; const TOKEN_DEFINITION_DATA_SIZE: usize = 23; diff --git a/nssa/src/program.rs b/nssa/src/program.rs index b256130..f91a007 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -7,7 +7,7 @@ use serde::Serialize; use crate::{ error::NssaError, - program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, + program_methods::{AUTHENTICATED_TRANSFER_ELF, MODIFIED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, }; /// Maximum number of cycles for a public execution. @@ -95,6 +95,12 @@ impl Program { // `program_methods` Self::new(TOKEN_ELF.to_vec()).unwrap() } + + pub fn modified_transfer_program() -> Self { + // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of + // `program_methods` + Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() + } } // TODO: Testnet only. Refactor to prevent compilation on mainnet. diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 409ceca..9359b04 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2386,4 +2386,70 @@ pub mod tests { assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))) } + + /// This test ensures that even if a malicious program tries to perform overflow of balances + /// it will not be able to break the balance validation. + #[test] + fn test_malicious_program_cannot_break_balance_validation() { + let sender_key = PrivateKey::try_new([37; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let sender_init_balance: u128 = 10; + + let recipient_key = PrivateKey::try_new([42; 32]).unwrap(); + let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key)); + let recipient_init_balance: u128 = 10; + + let mut state = V02State::new_with_genesis_accounts( + &[ + (sender_id, sender_init_balance), + (recipient_id, recipient_init_balance), + ], + &[], + ); + + state.insert_program(Program::modified_transfer_program()); + + let balance_to_move: u128 = 4; + + let sender = + AccountWithMetadata::new(state.get_account_by_id(&sender_id.clone()), true, sender_id); + + let sender_nonce = sender.account.nonce; + + let _recipient = + AccountWithMetadata::new(state.get_account_by_id(&recipient_id), false, sender_id); + + let message = public_transaction::Message::try_new( + Program::modified_transfer_program().id(), + vec![sender_id, recipient_id], + vec![sender_nonce], + balance_to_move, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); + let tx = PublicTransaction::new(message, witness_set); + let res = state.transition_from_public_transaction(&tx); + assert!(matches!(res, Err(NssaError::InvalidProgramBehavior))); + + let sender_post = state.get_account_by_id(&sender_id); + let recipient_post = state.get_account_by_id(&recipient_id); + + let expected_sender_post = { + let mut this = state.get_account_by_id(&sender_id); + this.balance = sender_init_balance; + this.nonce = 0; + this + }; + + let expected_recipient_post = { + let mut this = state.get_account_by_id(&sender_id); + this.balance = recipient_init_balance; + this.nonce = 0; + this + }; + + assert!(expected_sender_post == sender_post); + assert!(expected_recipient_post == recipient_post); + } } From 40991cf6d1be3540ccc292edeffb8043f6f67907 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Mon, 8 Dec 2025 12:11:11 +0200 Subject: [PATCH 39/70] fix: layered cleanup --- key_protocol/Cargo.toml | 2 + .../key_management/key_tree/chain_index.rs | 70 ++++- .../src/key_management/key_tree/mod.rs | 264 ++++++------------ key_protocol/src/key_protocol_core/mod.rs | 9 +- wallet/src/cli/mod.rs | 4 +- wallet/src/lib.rs | 3 +- 6 files changed, 157 insertions(+), 195 deletions(-) diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index a562515..24b92c0 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -16,9 +16,11 @@ bip39.workspace = true hmac-sha512.workspace = true thiserror.workspace = true nssa-core = { path = "../nssa/core", features = ["host"] } +itertools.workspace = true [dependencies.common] path = "../common" [dependencies.nssa] path = "../nssa" +features = ["no_docker"] diff --git a/key_protocol/src/key_management/key_tree/chain_index.rs b/key_protocol/src/key_management/key_tree/chain_index.rs index 8b28327..d2c9c3b 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -1,8 +1,9 @@ use std::{fmt::Display, str::FromStr}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize, Hash)] pub struct ChainIndex(Vec); #[derive(thiserror::Error, Debug)] @@ -104,6 +105,39 @@ impl ChainIndex { pub fn depth(&self) -> u32 { self.0.iter().map(|cci| cci + 1).sum() } + + fn collapse_back(&self) -> Option { + let mut res = self.parent()?; + + let last_mut = res.0.last_mut()?; + *last_mut += *(self.0.last()?) + 1; + + Some(res) + } + + fn shuffle_iter(&self) -> impl Iterator { + self.0 + .iter() + .permutations(self.0.len()) + .unique() + .map(|item| ChainIndex(item.into_iter().cloned().collect())) + } + + pub fn chain_ids_at_depth(depth: usize) -> impl Iterator { + let mut stack = vec![ChainIndex(vec![0; depth])]; + let mut cumulative_stack = vec![ChainIndex(vec![0; depth])]; + + while let Some(id) = stack.pop() { + if let Some(collapsed_id) = id.collapse_back() { + for id in collapsed_id.shuffle_iter() { + stack.push(id.clone()); + cumulative_stack.push(id); + } + } + } + + cumulative_stack.into_iter().unique() + } } #[cfg(test)] @@ -211,4 +245,38 @@ mod tests { assert_eq!(parent_chain_id, ChainIndex::root()) } + + #[test] + fn test_collapse_back() { + let chain_id = ChainIndex(vec![1, 1]); + + let collapsed = chain_id.collapse_back().unwrap(); + + assert_eq!(collapsed, ChainIndex(vec![3])) + } + + #[test] + fn test_collapse_back_one() { + let chain_id = ChainIndex(vec![1]); + + let collapsed = chain_id.collapse_back(); + + assert_eq!(collapsed, None) + } + + #[test] + fn test_collapse_back_root() { + let chain_id = ChainIndex(vec![]); + + let collapsed = chain_id.collapse_back(); + + assert_eq!(collapsed, None) + } + + #[test] + fn test_shuffle() { + for id in ChainIndex::chain_ids_at_depth(5) { + println!("{id}"); + } + } } diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index b75671e..d964af4 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -111,7 +111,7 @@ impl KeyTree { } } - fn generate_new_node_unconstrained( + pub fn generate_new_node( &mut self, parent_cci: &ChainIndex, ) -> Option<(nssa::AccountId, ChainIndex)> { @@ -165,7 +165,7 @@ impl KeyTree { let mut next_id = curr_id.nth_child(0); while (next_id.depth()) < depth { - self.generate_new_node_unconstrained(&curr_id); + self.generate_new_node(&curr_id); id_stack.push(next_id.clone()); next_id = next_id.next_in_line(); } @@ -174,45 +174,6 @@ impl KeyTree { } impl KeyTree { - #[allow(clippy::result_large_err)] - pub fn generate_new_node( - &mut self, - parent_cci: &ChainIndex, - ) -> Result<(nssa::AccountId, ChainIndex), KeyTreeGenerationError> { - let parent_keys = - self.key_map - .get(parent_cci) - .ok_or(KeyTreeGenerationError::ParentChainIdNotFound( - parent_cci.clone(), - ))?; - let next_child_id = self - .find_next_last_child_of_id(parent_cci) - .expect("Can be None only if parent is not present"); - let next_cci = parent_cci.nth_child(next_child_id); - - if let Some(prev_cci) = next_cci.previous_in_line() { - let prev_keys = self.key_map.get(&prev_cci).unwrap_or_else(|| { - panic!("Constraint violated, previous child with id {prev_cci} is missing") - }); - - if prev_keys.value.1 == nssa::Account::default() { - return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); - } - } else if *parent_cci != ChainIndex::root() - && parent_keys.value.1 == nssa::Account::default() - { - return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); - } - - let child_keys = parent_keys.nth_child(next_child_id); - let account_id = child_keys.account_id(); - - self.key_map.insert(next_cci.clone(), child_keys); - self.account_id_map.insert(account_id, next_cci.clone()); - - Ok((account_id, next_cci)) - } - /// Cleanup of all non-initialized accounts in a private tree /// /// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) < @@ -221,7 +182,9 @@ impl KeyTree { /// If account is default, removes them. /// /// Chain must be parsed for accounts beforehand - pub fn cleanup_tree_remove_ininit_for_depth(&mut self, depth: u32) { + /// + /// Fast, leaves gaps between accounts + pub fn cleanup_tree_remove_uninit_for_depth(&mut self, depth: u32) { let mut id_stack = vec![ChainIndex::root()]; while let Some(curr_id) = id_stack.pop() { @@ -241,64 +204,42 @@ impl KeyTree { } } } + + /// Cleanup of non-initialized accounts in a private tree + /// + /// If account is default, removes them, stops at first non-default account. + /// + /// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()` + /// + /// Chain must be parsed for accounts beforehand + /// + /// Slow, maintains tree consistency. + pub fn cleanup_tree_remove_uninit_layered(&mut self, depth: u32) { + 'outer: for i in (1..(depth as usize)).rev() { + println!("Cleanup of tree at depth {i}"); + for id in ChainIndex::chain_ids_at_depth(i) { + if let Some(node) = self.key_map.get(&id) { + if node.value.1 == nssa::Account::default() { + let addr = node.account_id(); + self.remove(addr); + } else { + break 'outer; + } + } + } + } + } } impl KeyTree { - #[allow(clippy::result_large_err)] - pub async fn generate_new_node( - &mut self, - parent_cci: &ChainIndex, - client: Arc, - ) -> Result<(nssa::AccountId, ChainIndex), KeyTreeGenerationError> { - let parent_keys = - self.key_map - .get(parent_cci) - .ok_or(KeyTreeGenerationError::ParentChainIdNotFound( - parent_cci.clone(), - ))?; - let next_child_id = self - .find_next_last_child_of_id(parent_cci) - .expect("Can be None only if parent is not present"); - let next_cci = parent_cci.nth_child(next_child_id); - - if let Some(prev_cci) = next_cci.previous_in_line() { - let prev_keys = self.key_map.get(&prev_cci).unwrap_or_else(|| { - panic!("Constraint violated, previous child with id {prev_cci} is missing") - }); - let prev_acc = client - .get_account(prev_keys.account_id().to_string()) - .await? - .account; - - if prev_acc == nssa::Account::default() { - return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); - } - } else if *parent_cci != ChainIndex::root() { - let parent_acc = client - .get_account(parent_keys.account_id().to_string()) - .await? - .account; - - if parent_acc == nssa::Account::default() { - return Err(KeyTreeGenerationError::PredecesorsNotInitialized(next_cci)); - } - } - - let child_keys = parent_keys.nth_child(next_child_id); - let account_id = child_keys.account_id(); - - self.key_map.insert(next_cci.clone(), child_keys); - self.account_id_map.insert(account_id, next_cci.clone()); - - Ok((account_id, next_cci)) - } - /// Cleanup of all non-initialized accounts in a public tree /// /// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) < /// depth`. /// /// If account is default, removes them. + /// + /// Fast, leaves gaps between accounts pub async fn cleanup_tree_remove_ininit_for_depth( &mut self, depth: u32, @@ -326,6 +267,38 @@ impl KeyTree { Ok(()) } + + /// Cleanup of non-initialized accounts in a public tree + /// + /// If account is default, removes them, stops at first non-default account. + /// + /// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()` + /// + /// Slow, maintains tree consistency. + pub async fn cleanup_tree_remove_uninit_layered( + &mut self, + depth: u32, + client: Arc, + ) -> Result<()> { + 'outer: for i in (1..(depth as usize)).rev() { + println!("Cleanup of tree at depth {i}"); + for id in ChainIndex::chain_ids_at_depth(i) { + if let Some(node) = self.key_map.get(&id) { + let address = node.account_id(); + let node_acc = client.get_account(address.to_string()).await?.account; + + if node_acc == nssa::Account::default() { + let addr = node.account_id(); + self.remove(addr); + } else { + break 'outer; + } + } + } + } + + Ok(()) + } } #[cfg(test)] @@ -367,8 +340,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -381,18 +353,12 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); let next_last_child_for_parent_id = tree .find_next_last_child_of_id(&ChainIndex::root()) @@ -413,8 +379,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -427,7 +392,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - let key_opt = tree.generate_new_node_unconstrained(&ChainIndex::from_str("/3").unwrap()); + let key_opt = tree.generate_new_node(&ChainIndex::from_str("/3").unwrap()); assert_eq!(key_opt, None); } @@ -444,8 +409,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -458,8 +422,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_node_unconstrained(&ChainIndex::root()) - .unwrap(); + tree.generate_new_node(&ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -472,7 +435,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 2); - tree.generate_new_node_unconstrained(&ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -486,7 +449,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/0").unwrap()) ); - tree.generate_new_node_unconstrained(&ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -500,7 +463,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/1").unwrap()) ); - tree.generate_new_node_unconstrained(&ChainIndex::from_str("/0").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -514,7 +477,7 @@ mod tests { .contains_key(&ChainIndex::from_str("/0/2").unwrap()) ); - tree.generate_new_node_unconstrained(&ChainIndex::from_str("/0/1").unwrap()) + tree.generate_new_node(&ChainIndex::from_str("/0/1").unwrap()) .unwrap(); assert!( @@ -529,49 +492,6 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); } - #[test] - fn test_key_generation_constraint() { - let seed_holder = seed_holder_for_tests(); - - let mut tree = KeyTreePrivate::new(&seed_holder); - - let (_, chain_id) = tree.generate_new_node(&ChainIndex::root()).unwrap(); - - assert_eq!(chain_id, ChainIndex::from_str("/0").unwrap()); - - let res = tree.generate_new_node(&ChainIndex::from_str("/").unwrap()); - - assert!(matches!( - res, - Err(KeyTreeGenerationError::PredecesorsNotInitialized(_)) - )); - - let res = tree.generate_new_node(&ChainIndex::from_str("/0").unwrap()); - - assert!(matches!( - res, - Err(KeyTreeGenerationError::PredecesorsNotInitialized(_)) - )); - - let acc = tree - .key_map - .get_mut(&ChainIndex::from_str("/0").unwrap()) - .unwrap(); - acc.value.1.balance = 1; - - let (_, chain_id) = tree - .generate_new_node(&ChainIndex::from_str("/").unwrap()) - .unwrap(); - - assert_eq!(chain_id, ChainIndex::from_str("/1").unwrap()); - - let (_, chain_id) = tree - .generate_new_node(&ChainIndex::from_str("/0").unwrap()) - .unwrap(); - - assert_eq!(chain_id, ChainIndex::from_str("/0/0").unwrap()); - } - #[test] fn test_cleanup() { let seed_holder = seed_holder_for_tests(); @@ -579,12 +499,6 @@ mod tests { let mut tree = KeyTreePrivate::new(&seed_holder); tree.generate_tree_for_depth(10); - let acc = tree - .key_map - .get_mut(&ChainIndex::from_str("/0").unwrap()) - .unwrap(); - acc.value.1.balance = 1; - let acc = tree .key_map .get_mut(&ChainIndex::from_str("/1").unwrap()) @@ -597,12 +511,6 @@ mod tests { .unwrap(); acc.value.1.balance = 3; - let acc = tree - .key_map - .get_mut(&ChainIndex::from_str("/0/0").unwrap()) - .unwrap(); - acc.value.1.balance = 4; - let acc = tree .key_map .get_mut(&ChainIndex::from_str("/0/1").unwrap()) @@ -615,7 +523,7 @@ mod tests { .unwrap(); acc.value.1.balance = 6; - tree.cleanup_tree_remove_ininit_for_depth(10); + tree.cleanup_tree_remove_uninit_layered(10); let mut key_set_res = HashSet::new(); key_set_res.insert("/0".to_string()); @@ -634,12 +542,6 @@ mod tests { assert_eq!(key_set, key_set_res); - let acc = tree - .key_map - .get(&ChainIndex::from_str("/0").unwrap()) - .unwrap(); - assert_eq!(acc.value.1.balance, 1); - let acc = tree .key_map .get(&ChainIndex::from_str("/1").unwrap()) @@ -652,12 +554,6 @@ mod tests { .unwrap(); assert_eq!(acc.value.1.balance, 3); - let acc = tree - .key_map - .get(&ChainIndex::from_str("/0/0").unwrap()) - .unwrap(); - assert_eq!(acc.value.1.balance, 4); - let acc = tree .key_map .get(&ChainIndex::from_str("/0/1").unwrap()) diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index c474df0..fc0a393 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -1,7 +1,6 @@ -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; use anyhow::Result; -use common::sequencer_client::SequencerClient; use k256::AffinePoint; use serde::{Deserialize, Serialize}; @@ -88,14 +87,12 @@ impl NSSAUserData { /// Generated new private key for public transaction signatures /// /// Returns the account_id of new account - pub async fn generate_new_public_transaction_private_key( + pub fn generate_new_public_transaction_private_key( &mut self, parent_cci: ChainIndex, - sequencer_client: Arc, ) -> nssa::AccountId { self.public_key_tree - .generate_new_node(&parent_cci, sequencer_client) - .await + .generate_new_node(&parent_cci) .unwrap() .0 } diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index cda599b..e53849e 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -219,7 +219,7 @@ pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<() .storage .user_data .public_key_tree - .cleanup_tree_remove_ininit_for_depth(depth, wallet_core.sequencer_client.clone()) + .cleanup_tree_remove_uninit_layered(depth, wallet_core.sequencer_client.clone()) .await?; println!("Public tree cleaned up"); @@ -240,7 +240,7 @@ pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<() .storage .user_data .private_key_tree - .cleanup_tree_remove_ininit_for_depth(depth); + .cleanup_tree_remove_uninit_layered(depth); println!("Private tree cleaned up"); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 56872db..d0a9014 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -115,8 +115,7 @@ impl WalletCore { pub async fn create_new_account_public(&mut self, chain_index: ChainIndex) -> AccountId { self.storage .user_data - .generate_new_public_transaction_private_key(chain_index, self.sequencer_client.clone()) - .await + .generate_new_public_transaction_private_key(chain_index) } pub fn create_new_account_private(&mut self, chain_index: ChainIndex) -> AccountId { From 01f7acb4d68e982d6d071092dbbdcc733b01873a Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Mon, 8 Dec 2025 12:13:03 +0200 Subject: [PATCH 40/70] fix: remove unused enum --- key_protocol/src/key_management/key_tree/mod.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index d964af4..9b5014f 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::Result; -use common::{error::SequencerClientError, sequencer_client::SequencerClient}; +use common::sequencer_client::SequencerClient; use serde::{Deserialize, Serialize}; use crate::key_management::{ @@ -29,16 +29,6 @@ pub struct KeyTree { pub type KeyTreePublic = KeyTree; pub type KeyTreePrivate = KeyTree; -#[derive(thiserror::Error, Debug)] -pub enum KeyTreeGenerationError { - #[error("Parent chain id {0} not present in tree")] - ParentChainIdNotFound(ChainIndex), - #[error("Parent or left relative of {0} is not initialized")] - PredecesorsNotInitialized(ChainIndex), - #[error("Sequencer client error {0:#?}")] - SequencerClientError(#[from] SequencerClientError), -} - impl KeyTree { pub fn new(seed: &SeedHolder) -> Self { let seed_fit: [u8; 64] = seed From f1760b67ee06923a7dbb0e27a416d5ed5393273d Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Mon, 8 Dec 2025 12:46:43 +0200 Subject: [PATCH 41/70] fix: revert difficulty --- nssa/src/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 5434636..cef7791 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -233,8 +233,8 @@ impl V02State { Account { program_owner: Program::pinata().id(), balance: 1500, - // Difficulty: 2 - data: vec![2; 33], + // Difficulty: 3 + data: vec![3; 33], nonce: 0, }, ); From ddeeab7d8067ed559c4b2184bdad73cabc53b6cf Mon Sep 17 00:00:00 2001 From: Pravdyvy <46261001+Pravdyvy@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:50:41 +0200 Subject: [PATCH 42/70] Apply suggestions from code review Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com> --- integration_tests/src/test_suite_map.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 2819d03..bb7260a 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -603,13 +603,13 @@ pub fn prepare_function_map() -> HashMap { } /// This test creates a new private token using the token program. All accounts are owned except - /// suply. + /// supply. #[nssa_integration_test] pub async fn test_success_token_program_private_owned_definition() { info!("########## test_success_token_program_private_owned_definition ##########"); let wallet_config = fetch_config().await.unwrap(); - // Create new account for the token definition (public) + // Create new account for the token definition (private) let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( @@ -622,7 +622,7 @@ pub fn prepare_function_map() -> HashMap { else { panic!("invalid subcommand return value"); }; - // Create new account for the token supply holder (private) + // Create new account for the token supply holder (public) let SubcommandReturnValue::RegisterAccount { account_id: supply_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( @@ -693,7 +693,7 @@ pub fn prepare_function_map() -> HashMap { ); let wallet_config = fetch_config().await.unwrap(); - // Create new account for the token definition (public) + // Create new account for the token definition (private) let SubcommandReturnValue::RegisterAccount { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( From ea19ad0c1f6e10e450fa5dbcf950e8cb01e8294a Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Mon, 8 Dec 2025 13:00:03 +0200 Subject: [PATCH 43/70] fix: comments fix --- integration_tests/src/test_suite_map.rs | 40 +++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index bb7260a..a83314f 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -441,8 +441,8 @@ pub fn prepare_function_map() -> HashMap { } /// This test creates a new private token using the token program. After creating the token, the - /// test executes a private token transfer to a new account. All accounts are owned except - /// definition. + /// test executes a private token transfer to a new account. All accounts are private owned + /// except definition which is public. #[nssa_integration_test] pub async fn test_success_token_program_private_owned_supply() { info!("########## test_success_token_program_private_owned_supply ##########"); @@ -602,8 +602,8 @@ pub fn prepare_function_map() -> HashMap { assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await); } - /// This test creates a new private token using the token program. All accounts are owned except - /// supply. + /// This test creates a new private token using the token program. All accounts are private + /// owned except supply which is public. #[nssa_integration_test] pub async fn test_success_token_program_private_owned_definition() { info!("########## test_success_token_program_private_owned_definition ##########"); @@ -685,7 +685,8 @@ pub fn prepare_function_map() -> HashMap { ); } - /// This test creates a new private token using the token program. All accounts are owned. + /// This test creates a new private token using the token program. All accounts are private + /// owned. #[nssa_integration_test] pub async fn test_success_token_program_private_owned_definition_and_supply() { info!( @@ -753,6 +754,35 @@ pub fn prepare_function_map() -> HashMap { .get_private_account_commitment(&supply_account_id) .unwrap(); assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await); + + let definition_acc = wallet_storage + .get_account_private(&definition_account_id) + .unwrap(); + let supply_acc = wallet_storage + .get_account_private(&supply_account_id) + .unwrap(); + + 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 + ] + ); + + assert_eq!(supply_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!( + supply_acc.data, + vec![ + 1, 128, 101, 5, 31, 43, 36, 97, 108, 164, 92, 25, 157, 173, 5, 14, 194, 121, 239, + 84, 19, 160, 243, 47, 193, 2, 250, 247, 232, 253, 191, 232, 173, 37, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); } /// This test creates a new private token using the token program. After creating the token, the From 80a188e6a375ec5d03e2a094ba973b02de0af941 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 9 Dec 2025 00:07:10 +0300 Subject: [PATCH 44/70] feat: remove print spam when running wallet account sync-private --- wallet/Cargo.toml | 1 + wallet/src/chain_storage.rs | 3 ++- wallet/src/lib.rs | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 6f97c63..c93b357 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -23,6 +23,7 @@ itertools.workspace = true sha2.workspace = true futures.workspace = true async-stream = "0.3.6" +indicatif = { version = "0.18.3", features = ["improved_unicode"] } [dependencies.key_protocol] path = "../key_protocol" diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index 0625fce..68d685f 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -8,6 +8,7 @@ use key_protocol::{ }, key_protocol_core::NSSAUserData, }; +use log::debug; use nssa::program::Program; use crate::config::{InitialAccountData, PersistentAccountData, WalletConfig}; @@ -127,7 +128,7 @@ impl WalletChainStore { account_id: nssa::AccountId, account: nssa_core::account::Account, ) { - println!("inserting at address {account_id}, this account {account:?}"); + debug!("inserting at address {account_id}, this account {account:?}"); let entry = self .user_data diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 91a0e4b..b27115c 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -306,11 +306,14 @@ impl WalletCore { } let before_polling = std::time::Instant::now(); + let num_of_blocks = block_id - self.last_synced_block; + println!("Syncing to block {block_id}. Blocks to sync: {num_of_blocks}"); let poller = self.poller.clone(); let mut blocks = std::pin::pin!(poller.poll_block_range(self.last_synced_block + 1..=block_id)); + let bar = indicatif::ProgressBar::new(num_of_blocks); while let Some(block) = blocks.try_next().await? { for tx in block.transactions { let nssa_tx = NSSATransaction::try_from(&tx)?; @@ -319,7 +322,9 @@ impl WalletCore { self.last_synced_block = block.block_id; self.store_persistent_data().await?; + bar.inc(1); } + bar.finish(); println!( "Synced to block {block_id} in {:?}", @@ -379,7 +384,7 @@ impl WalletCore { .collect::>(); for (affected_account_id, new_acc) in affected_accounts { - println!( + info!( "Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}" ); self.storage From 47ff7f0b64cc0e1c6e936fd8a02eaef993ea38fe Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 9 Dec 2025 11:09:15 +0200 Subject: [PATCH 45/70] fix: layered automatization --- key_protocol/Cargo.toml | 1 - .../src/key_management/key_tree/mod.rs | 74 +++++++------------ key_protocol/src/key_protocol_core/mod.rs | 8 +- 3 files changed, 31 insertions(+), 52 deletions(-) diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 24b92c0..103a1de 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -23,4 +23,3 @@ path = "../common" [dependencies.nssa] path = "../nssa" -features = ["no_docker"] diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 9f72ecf..324d6fe 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeMap, HashMap, VecDeque}, + collections::{BTreeMap, HashMap}, sync::Arc, }; @@ -122,38 +122,34 @@ impl KeyTree { Some((account_id, next_cci)) } - fn have_child_slot_capped(&self, cci: &ChainIndex) -> bool { - let depth = cci.depth(); + fn find_next_slot_layered(&self) -> ChainIndex { + let mut depth = 1; - self.find_next_last_child_of_id(cci) - .map(|inn| inn + 1 + depth < DEPTH_SOFT_CAP) - .unwrap_or(false) - } - - pub fn search_new_parent_capped(&self) -> Option { - let mut parent_list = VecDeque::new(); - parent_list.push_front(ChainIndex::root()); - - let mut search_res = None; - - while let Some(next_parent) = parent_list.pop_back() { - if self.have_child_slot_capped(&next_parent) { - search_res = Some(next_parent); - break; - } else { - let last_child = self.find_next_last_child_of_id(&next_parent)?; - - for id in 0..last_child { - parent_list.push_front(next_parent.nth_child(id)); + 'outer: loop { + for chain_id in ChainIndex::chain_ids_at_depth(depth) { + if self.key_map.get(&chain_id).is_none() { + break 'outer chain_id; } } + depth += 1; } - - search_res } - pub fn generate_new_node_capped(&mut self) -> Option<(nssa::AccountId, ChainIndex)> { - self.generate_new_node(&self.search_new_parent_capped()?) + pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option<(nssa::AccountId, ChainIndex)> { + let parent_keys = self.key_map.get(&chain_index.parent()?)?; + let child_id = *chain_index.chain().last()?; + + let child_keys = parent_keys.nth_child(child_id); + let account_id = child_keys.account_id(); + + self.key_map.insert(chain_index.clone(), child_keys); + self.account_id_map.insert(account_id, chain_index.clone()); + + Some((account_id, chain_index.clone())) + } + + pub fn generate_new_node_layered(&mut self) -> Option<(nssa::AccountId, ChainIndex)> { + self.fill_node(&self.find_next_slot_layered()) } pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> { @@ -524,29 +520,13 @@ mod tests { let mut tree = KeyTreePublic::new(&seed_holder); - for _ in 0..19 { - tree.generate_new_node_capped().unwrap(); - } + let next_slot = tree.find_next_slot_layered(); - let next_suitable_parent = tree.search_new_parent_capped().unwrap(); + println!("NEXT SLOT {next_slot}"); - assert_eq!(next_suitable_parent, ChainIndex::from_str("/0").unwrap()); + let (acc_id, chain_id) = tree.generate_new_node_layered().unwrap(); - for _ in 0..18 { - tree.generate_new_node_capped().unwrap(); - } - - let next_suitable_parent = tree.search_new_parent_capped().unwrap(); - - assert_eq!(next_suitable_parent, ChainIndex::from_str("/1").unwrap()); - - for _ in 0..17 { - tree.generate_new_node_capped().unwrap(); - } - - let next_suitable_parent = tree.search_new_parent_capped().unwrap(); - - assert_eq!(next_suitable_parent, ChainIndex::from_str("/2").unwrap()); + println!("NEXT ACC {acc_id} at {chain_id}"); } #[test] diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index ce41d38..b46c46c 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -98,8 +98,8 @@ impl NSSAUserData { .expect("Parent must be present in a tree"), None => self .public_key_tree - .generate_new_node_capped() - .expect("No slots left"), + .generate_new_node_layered() + .expect("Search for new node slot failed"), } } @@ -131,8 +131,8 @@ impl NSSAUserData { .expect("Parent must be present in a tree"), None => self .private_key_tree - .generate_new_node_capped() - .expect("No slots left"), + .generate_new_node_layered() + .expect("Search for new node slot failed"), } } From eed485bd2ca42db1193a8b560418e6ac89bf80e1 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 9 Dec 2025 11:10:40 +0200 Subject: [PATCH 46/70] fix: tomil fix --- key_protocol/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 24b92c0..103a1de 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -23,4 +23,3 @@ path = "../common" [dependencies.nssa] path = "../nssa" -features = ["no_docker"] From e1eff074783a639893984b3597f0da2cdbd29a69 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 9 Dec 2025 11:51:44 +0200 Subject: [PATCH 47/70] fix: layered autobalancing --- .../src/key_management/key_tree/chain_index.rs | 16 ++++++++++++++++ key_protocol/src/key_management/key_tree/mod.rs | 14 +++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/key_protocol/src/key_management/key_tree/chain_index.rs b/key_protocol/src/key_management/key_tree/chain_index.rs index d2c9c3b..6dbaf9a 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -138,6 +138,22 @@ impl ChainIndex { cumulative_stack.into_iter().unique() } + + pub fn chain_ids_at_depth_rev(depth: usize) -> impl Iterator { + let mut stack = vec![ChainIndex(vec![0; depth])]; + let mut cumulative_stack = vec![ChainIndex(vec![0; depth])]; + + while let Some(id) = stack.pop() { + if let Some(collapsed_id) = id.collapse_back() { + for id in collapsed_id.shuffle_iter() { + stack.push(id.clone()); + cumulative_stack.push(id); + } + } + } + + cumulative_stack.into_iter().rev().unique() + } } #[cfg(test)] diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 324d6fe..389580b 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -126,8 +126,8 @@ impl KeyTree { let mut depth = 1; 'outer: loop { - for chain_id in ChainIndex::chain_ids_at_depth(depth) { - if self.key_map.get(&chain_id).is_none() { + for chain_id in ChainIndex::chain_ids_at_depth_rev(depth) { + if !self.key_map.contains_key(&chain_id) { break 'outer chain_id; } } @@ -520,13 +520,13 @@ mod tests { let mut tree = KeyTreePublic::new(&seed_holder); + for _ in 0..100 { + tree.generate_new_node_layered().unwrap(); + } + let next_slot = tree.find_next_slot_layered(); - println!("NEXT SLOT {next_slot}"); - - let (acc_id, chain_id) = tree.generate_new_node_layered().unwrap(); - - println!("NEXT ACC {acc_id} at {chain_id}"); + assert_eq!(next_slot, ChainIndex::from_str("/0/0/2/1").unwrap()); } #[test] From e0c6baf1ffff1b9a84d4a25c78fbcb30e1c34540 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Tue, 9 Dec 2025 08:11:15 -0500 Subject: [PATCH 48/70] added mint overflow protection --- nssa/program_methods/guest/src/bin/token.rs | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index c229441..a5aed8b 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -318,9 +318,6 @@ fn mint_additional_supply(pre_states: &[AccountWithMetadata], amount_to_mint: u1 let definition_values = TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); - //TODO: add overflow protection - // TokenDefinition.supply_limit + amount_to_mint - let token_holding_values: TokenHolding = if token_holding.account == Account::default() { TokenHolding::new(&definition.account_id) } else { TokenHolding::parse(&token_holding.account.data).expect("Holding account must be valid") }; @@ -335,10 +332,15 @@ fn mint_additional_supply(pre_states: &[AccountWithMetadata], amount_to_mint: u1 balance: token_holding_values.balance + amount_to_mint, }; + let post_total_supply = definition_values + .total_supply + .checked_add(amount_to_mint) + .expect("Total supply overflow."); + let post_definition_data = TokenDefinition { account_type: definition_values.account_type, name: definition_values.name, - total_supply: definition_values.total_supply + amount_to_mint, + total_supply: post_total_supply, }; let post_definition = { @@ -841,6 +843,7 @@ mod tests { MintSuccess, InitSupplyMint, HoldingBalanceMint, + MintOverflow, } enum AccountsEnum { @@ -1041,6 +1044,7 @@ mod tests { BalanceEnum::MintSuccess => 50_000, BalanceEnum::InitSupplyMint => 150_000, BalanceEnum::HoldingBalanceMint => 51_000, + BalanceEnum::MintOverflow => (2 as u128).pow(128) - 40_000, _ => panic!("Invalid selection") } } @@ -1177,4 +1181,16 @@ mod tests { assert!(holding_post.requires_claim() == true); } + #[test] + #[should_panic(expected = "Total supply overflow.")] + fn test_mint_overflow() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + ]; + let _post_states = mint_additional_supply(&pre_states, + helper_balance_constructor(BalanceEnum::MintOverflow)); + + } + } \ No newline at end of file From 46ac451284a769879c3f5894cd045776c8217e4f Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Mon, 8 Dec 2025 18:26:35 +0300 Subject: [PATCH 49/70] feat: add basic auth to wallet --- common/src/sequencer_client.rs | 16 ++++++- .../guest/src/bin/pinata_token.rs | 17 +++++-- .../src/bin/privacy_preserving_circuit.rs | 7 ++- nssa/program_methods/guest/src/bin/token.rs | 31 ++++++------ wallet/src/chain_storage.rs | 1 + wallet/src/cli/config.rs | 13 +++++ wallet/src/cli/mod.rs | 24 +++++++++- wallet/src/config.rs | 48 +++++++++++++++++++ wallet/src/helperfunctions.rs | 19 +++++++- wallet/src/lib.rs | 18 ++++++- wallet/src/main.rs | 14 ++++-- 11 files changed, 170 insertions(+), 38 deletions(-) diff --git a/common/src/sequencer_client.rs b/common/src/sequencer_client.rs index d3c5f23..622c0c1 100644 --- a/common/src/sequencer_client.rs +++ b/common/src/sequencer_client.rs @@ -30,16 +30,25 @@ use crate::{ pub struct SequencerClient { pub client: reqwest::Client, pub sequencer_addr: String, + pub basic_auth: Option<(String, Option)>, } impl SequencerClient { pub fn new(sequencer_addr: String) -> Result { + Self::new_with_auth(sequencer_addr, None) + } + + pub fn new_with_auth( + sequencer_addr: String, + basic_auth: Option<(String, Option)>, + ) -> Result { Ok(Self { client: Client::builder() //Add more fiedls if needed .timeout(std::time::Duration::from_secs(60)) .build()?, sequencer_addr, + basic_auth, }) } @@ -51,13 +60,16 @@ impl SequencerClient { let request = rpc_primitives::message::Request::from_payload_version_2_0(method.to_string(), payload); - let call_builder = self.client.post(&self.sequencer_addr); + let mut call_builder = self.client.post(&self.sequencer_addr); + + if let Some((username, password)) = &self.basic_auth { + call_builder = call_builder.basic_auth(username, password.as_deref()); + } let call_res = call_builder.json(&request).send().await?; let response_vall = call_res.json::().await?; - // TODO: Actually why we need separation of `result` and `error` in rpc response? #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct SequencerRpcResponse { diff --git a/nssa/program_methods/guest/src/bin/pinata_token.rs b/nssa/program_methods/guest/src/bin/pinata_token.rs index be661c2..91d887a 100644 --- a/nssa/program_methods/guest/src/bin/pinata_token.rs +++ b/nssa/program_methods/guest/src/bin/pinata_token.rs @@ -1,8 +1,11 @@ use nssa_core::program::{ - read_nssa_inputs, write_nssa_outputs_with_chained_call, AccountPostState, ChainedCall, PdaSeed, ProgramInput + AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, +}; +use risc0_zkvm::{ + serde::to_vec, + sha::{Impl, Sha256}, }; -use risc0_zkvm::serde::to_vec; -use risc0_zkvm::sha::{Impl, Sha256}; const PRIZE: u128 = 150; @@ -46,7 +49,8 @@ impl Challenge { /// A pinata program fn main() { // Read input accounts. - // It is expected to receive three accounts: [pinata_definition, pinata_token_holding, winner_token_holding] + // It is expected to receive three accounts: [pinata_definition, pinata_token_holding, + // winner_token_holding] let ProgramInput { pre_states, instruction: solution, @@ -83,7 +87,10 @@ fn main() { let chained_calls = vec![ChainedCall { program_id: pinata_token_holding_post.program_owner, instruction_data: to_vec(&instruction_data).unwrap(), - pre_states: vec![pinata_token_holding_for_chain_call, winner_token_holding.clone()], + pre_states: vec![ + pinata_token_holding_for_chain_call, + winner_token_holding.clone(), + ], pda_seeds: vec![PdaSeed::new([0; 32])], }]; 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 7813fa5..ac4e212 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,15 +1,14 @@ use std::collections::HashSet; -use risc0_zkvm::{guest::env, serde::to_vec}; - use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, - Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, + NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution}, }; +use risc0_zkvm::{guest::env, serde::to_vec}; fn main() { let PrivacyPreservingCircuitInput { diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index bb433d1..5ac3f97 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -6,25 +6,22 @@ use nssa_core::{ }; // The token program has three functions: -// 1. New token definition. -// Arguments to this function are: -// * Two **default** accounts: [definition_account, holding_account]. -// 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)] -// The name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -// 2. Token transfer -// Arguments to this function are: +// 1. New token definition. Arguments to this function are: +// * Two **default** accounts: [definition_account, holding_account]. 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)] 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 with the following layout -// [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. -// 3. Initialize account with zero balance -// Arguments to this function are: +// * 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]. +// 3. Initialize account with zero balance Arguments to this function are: // * Two accounts: [definition_account, account_to_initialize]. -// * An dummy byte string of length 23, with the following layout -// [0x02 || 0x00 || 0x00 || 0x00 || ... || 0x00 || 0x00]. +// * An dummy byte string of length 23, with the following layout [0x02 || 0x00 || 0x00 || 0x00 +// || ... || 0x00 || 0x00]. const TOKEN_DEFINITION_TYPE: u8 = 0; const TOKEN_DEFINITION_DATA_SIZE: usize = 23; diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index 0625fce..1223d1f 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -263,6 +263,7 @@ mod tests { seq_poll_max_retries: 10, seq_block_poll_max_amount: 100, initial_accounts: create_initial_accounts(), + basic_auth: None, } } diff --git a/wallet/src/cli/config.rs b/wallet/src/cli/config.rs index df0413e..d4b3f5a 100644 --- a/wallet/src/cli/config.rs +++ b/wallet/src/cli/config.rs @@ -73,6 +73,13 @@ impl WalletSubcommand for ConfigSubcommand { "initial_accounts" => { println!("{:#?}", wallet_core.storage.wallet_config.initial_accounts); } + "basic_auth" => { + if let Some(basic_auth) = &wallet_core.storage.wallet_config.basic_auth { + println!("{basic_auth}"); + } else { + println!("Not set"); + } + } _ => { println!("Unknown field"); } @@ -99,6 +106,9 @@ impl WalletSubcommand for ConfigSubcommand { wallet_core.storage.wallet_config.seq_block_poll_max_amount = value.parse()?; } + "basic_auth" => { + wallet_core.storage.wallet_config.basic_auth = Some(value.parse()?); + } "initial_accounts" => { anyhow::bail!("Setting this field from wallet is not supported"); } @@ -141,6 +151,9 @@ impl WalletSubcommand for ConfigSubcommand { "initial_accounts" => { println!("List of initial accounts' keys(both public and private)"); } + "basic_auth" => { + println!("Basic authentication credentials for sequencer HTTP requests"); + } _ => { println!("Unknown field"); } diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index eb4e891..b3d40b8 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -13,7 +13,7 @@ use crate::{ token::TokenProgramAgnosticSubcommand, }, }, - helperfunctions::fetch_config, + helperfunctions::{fetch_config, merge_auth_config}, }; pub mod account; @@ -69,7 +69,7 @@ pub enum OverCommand { /// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config /// -/// All account adresses must be valid 32 byte base58 strings. +/// All account addresses must be valid 32 byte base58 strings. /// /// All account account_ids must be provided as {privacy_prefix}/{account_id}, /// where valid options for `privacy_prefix` is `Public` and `Private` @@ -79,6 +79,9 @@ pub struct Args { /// Continious run flag #[arg(short, long)] pub continuous_run: bool, + /// Basic authentication in the format `user` or `user:password` + #[arg(long)] + pub auth: Option, /// Wallet command #[command(subcommand)] pub command: Option, @@ -94,7 +97,15 @@ pub enum SubcommandReturnValue { } pub async fn execute_subcommand(command: Command) -> Result { + execute_subcommand_with_auth(command, None).await +} + +pub async fn execute_subcommand_with_auth( + command: Command, + auth: Option, +) -> Result { let wallet_config = fetch_config().await?; + let wallet_config = merge_auth_config(wallet_config, auth)?; let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?; let subcommand_ret = match command { @@ -160,7 +171,11 @@ pub async fn execute_subcommand(command: Command) -> Result Result<()> { + execute_continuous_run_with_auth(None).await +} +pub async fn execute_continuous_run_with_auth(auth: Option) -> Result<()> { let config = fetch_config().await?; + let config = merge_auth_config(config, auth)?; let mut wallet_core = WalletCore::start_from_config_update_chain(config.clone()).await?; loop { @@ -179,7 +194,12 @@ pub async fn execute_continuous_run() -> Result<()> { } pub async fn execute_setup(password: String) -> Result<()> { + execute_setup_with_auth(password, None).await +} + +pub async fn execute_setup_with_auth(password: String, auth: Option) -> Result<()> { let config = fetch_config().await?; + let config = merge_auth_config(config, auth)?; let wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password).await?; wallet_core.store_persistent_data().await?; diff --git a/wallet/src/config.rs b/wallet/src/config.rs index ebcf283..c06ccc4 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use key_protocol::key_management::{ KeyChain, key_tree::{ @@ -6,6 +8,49 @@ use key_protocol::key_management::{ }; use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BasicAuth { + pub username: String, + pub password: Option, +} + +impl std::fmt::Display for BasicAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.username)?; + if let Some(password) = &self.password { + write!(f, ":{password}")?; + } + + Ok(()) + } +} + +impl FromStr for BasicAuth { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let parse = || { + let mut parts = s.splitn(2, ':'); + let username = parts.next()?; + let password = parts.next().filter(|p| !p.is_empty()); + if parts.next().is_some() { + return None; + } + + Some((username, password)) + }; + + let (username, password) = parse().ok_or_else(|| { + anyhow::anyhow!("Invalid auth format. Expected 'user' or 'user:password'") + })?; + + Ok(Self { + username: username.to_string(), + password: password.map(|p| p.to_string()), + }) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InitialAccountDataPublic { pub account_id: String, @@ -143,6 +188,8 @@ pub struct WalletConfig { pub seq_block_poll_max_amount: u64, /// Initial accounts for wallet pub initial_accounts: Vec, + /// Basic authentication credentials + pub basic_auth: Option, } impl Default for WalletConfig { @@ -154,6 +201,7 @@ impl Default for WalletConfig { seq_tx_poll_max_blocks: 5, seq_poll_max_retries: 5, seq_block_poll_max_amount: 100, + basic_auth: None, initial_accounts: { let init_acc_json = r#" [ diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 770d2bb..5f1dcf7 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -12,7 +12,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ HOME_DIR_ENV_VAR, config::{ - InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, + BasicAuth, InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig, }, }; @@ -89,6 +89,23 @@ pub async fn fetch_config() -> Result { Ok(config) } +/// Parse CLI auth string and merge with config auth, prioritizing CLI +pub fn merge_auth_config( + mut config: WalletConfig, + cli_auth: Option, +) -> Result { + if let Some(auth_str) = cli_auth { + let cli_auth_config: BasicAuth = auth_str.parse()?; + + if config.basic_auth.is_some() { + println!("Warning: CLI auth argument takes precedence over config basic-auth"); + } + + config.basic_auth = Some(cli_auth_config); + } + Ok(config) +} + /// Fetch data stored at home /// /// File must be created through setup beforehand. diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 91a0e4b..11f4a4a 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -47,7 +47,14 @@ pub struct WalletCore { impl WalletCore { pub async fn start_from_config_update_chain(config: WalletConfig) -> Result { - let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); + let basic_auth = config + .basic_auth + .as_ref() + .map(|auth| (auth.username.clone(), auth.password.clone())); + let client = Arc::new(SequencerClient::new_with_auth( + config.sequencer_addr.clone(), + basic_auth, + )?); let tx_poller = TxPoller::new(config.clone(), client.clone()); let PersistentStorage { @@ -69,7 +76,14 @@ impl WalletCore { config: WalletConfig, password: String, ) -> Result { - let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); + let basic_auth = config + .basic_auth + .as_ref() + .map(|auth| (auth.username.clone(), auth.password.clone())); + let client = Arc::new(SequencerClient::new_with_auth( + config.sequencer_addr.clone(), + basic_auth, + )?); let tx_poller = TxPoller::new(config.clone(), client.clone()); let storage = WalletChainStore::new_storage(config, password)?; diff --git a/wallet/src/main.rs b/wallet/src/main.rs index a8a4fbe..27d102d 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -1,7 +1,10 @@ use anyhow::Result; use clap::{CommandFactory as _, Parser as _}; use tokio::runtime::Builder; -use wallet::cli::{Args, OverCommand, execute_continuous_run, execute_setup, execute_subcommand}; +use wallet::cli::{ + Args, OverCommand, execute_continuous_run_with_auth, execute_setup_with_auth, + execute_subcommand_with_auth, +}; pub const NUM_THREADS: usize = 2; @@ -10,7 +13,6 @@ pub const NUM_THREADS: usize = 2; // file path? // TODO #172: Why it requires config as env var while sequencer_runner accepts as // argument? -// TODO #171: Running pinata doesn't give output about transaction hash and etc. fn main() -> Result<()> { let runtime = Builder::new_multi_thread() .worker_threads(NUM_THREADS) @@ -26,13 +28,15 @@ fn main() -> Result<()> { if let Some(over_command) = args.command { match over_command { OverCommand::Command(command) => { - let _output = execute_subcommand(command).await?; + let _output = execute_subcommand_with_auth(command, args.auth).await?; Ok(()) } - OverCommand::Setup { password } => execute_setup(password).await, + OverCommand::Setup { password } => { + execute_setup_with_auth(password, args.auth).await + } } } else if args.continuous_run { - execute_continuous_run().await + execute_continuous_run_with_auth(args.auth).await } else { let help = Args::command().render_long_help(); println!("{help}"); From e5cbd82343a534bb7fea62b3de908a415429746d Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sat, 6 Dec 2025 21:37:51 +0300 Subject: [PATCH 50/70] fix: add overflow check for balance sum validation --- nssa/core/src/program.rs | 42 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 2d38fa4..04e91c1 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -204,8 +204,19 @@ pub fn validate_execution( } // 7. Total balance is preserved - let total_balance_pre_states: u128 = pre_states.iter().map(|pre| pre.account.balance).sum(); - let total_balance_post_states: u128 = post_states.iter().map(|post| post.account.balance).sum(); + + let Some(total_balance_pre_states) = + WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) + else { + return false; + }; + + let Some(total_balance_post_states) = + WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance)) + else { + return false; + }; + if total_balance_pre_states != total_balance_post_states { return false; } @@ -213,6 +224,33 @@ pub fn validate_execution( true } +/// Representation of a number as `lo + hi * 2^128`. +#[derive(PartialEq, Eq)] +struct WrappedBalanceSum { + lo: u128, + hi: u128, +} + +impl WrappedBalanceSum { + /// Constructs a [`WrappedBalanceSum`] from an iterator of balances. + /// + /// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not + /// expected in practical scenarios. + fn from_balances(balances: impl Iterator) -> Option { + let mut wrapped = WrappedBalanceSum { lo: 0, hi: 0 }; + + for balance in balances { + let (new_sum, did_overflow) = wrapped.lo.overflowing_add(balance); + if did_overflow { + wrapped.hi = wrapped.hi.checked_add(1)?; + } + wrapped.lo = new_sum; + } + + Some(wrapped) + } +} + #[cfg(test)] mod tests { use super::*; From 4574acfc49fe0b81f2176be4106cbdcfe26338c7 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Fri, 5 Dec 2025 02:17:09 +0300 Subject: [PATCH 51/70] feat: introduce account data size limit --- integration_tests/src/test_suite_map.rs | 29 +-- integration_tests/src/tps_test_utils.rs | 7 +- nssa/core/Cargo.toml | 7 +- nssa/core/src/account.rs | 9 +- nssa/core/src/account/data.rs | 175 ++++++++++++++++++ nssa/core/src/circuit_io.rs | 6 +- nssa/core/src/encoding.rs | 13 +- nssa/core/src/program.rs | 6 +- nssa/program_methods/guest/Cargo.lock | 1 + nssa/program_methods/guest/src/bin/pinata.rs | 6 +- .../guest/src/bin/pinata_token.rs | 15 +- nssa/program_methods/guest/src/bin/token.rs | 89 +++++---- .../privacy_preserving_transaction/circuit.rs | 8 +- nssa/src/state.rs | 24 +-- nssa/test_program_methods/guest/Cargo.lock | 1 + .../guest/src/bin/data_changer.rs | 4 +- wallet/src/cli/programs/pinata.rs | 1 + 17 files changed, 312 insertions(+), 89 deletions(-) create mode 100644 nssa/core/src/account/data.rs diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 1c5f91f..17caf26 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -356,8 +356,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -375,11 +375,14 @@ pub fn prepare_function_map() -> HashMap { // 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); + assert_eq!(supply_acc.data.as_ref()[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 account_id of the token definition account. - assert_eq!(&supply_acc.data[1..33], definition_account_id.to_bytes()); + assert_eq!( + &supply_acc.data.as_ref()[1..33], + definition_account_id.to_bytes() + ); assert_eq!( u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()), 37 @@ -518,8 +521,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -679,8 +682,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -821,8 +824,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -963,8 +966,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -1480,7 +1483,7 @@ pub fn prepare_function_map() -> HashMap { .account; assert_eq!(post_state_account.program_owner, data_changer.id()); assert_eq!(post_state_account.balance, 0); - assert_eq!(post_state_account.data, vec![0]); + assert_eq!(post_state_account.data.as_ref(), &[0]); assert_eq!(post_state_account.nonce, 0); info!("Success!"); diff --git a/integration_tests/src/tps_test_utils.rs b/integration_tests/src/tps_test_utils.rs index d06a08f..29462f6 100644 --- a/integration_tests/src/tps_test_utils.rs +++ b/integration_tests/src/tps_test_utils.rs @@ -8,7 +8,8 @@ use nssa::{ public_transaction as putx, }; use nssa_core::{ - MembershipProof, NullifierPublicKey, account::AccountWithMetadata, + MembershipProof, NullifierPublicKey, + account::{AccountWithMetadata, data::Data}, encryption::IncomingViewingPublicKey, }; use sequencer_core::config::{AccountInitialData, CommitmentsInitialData, SequencerConfig}; @@ -90,7 +91,7 @@ impl TpsTestManager { balance: 100, nonce: 0xdeadbeef, program_owner: Program::authenticated_transfer_program().id(), - data: vec![], + data: Data::default(), }; let initial_commitment = CommitmentsInitialData { npk: sender_npk, @@ -129,7 +130,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { balance: 100, nonce: 0xdeadbeef, program_owner: program.id(), - data: vec![], + data: Data::default(), }, true, AccountId::from(&sender_npk), diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index 0e16a3f..f179899 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] risc0-zkvm = { version = "3.0.3", features = ['std'] } serde = { version = "1.0", default-features = false } -thiserror = { version = "2.0.12", optional = true } +thiserror = { version = "2.0.12" } bytemuck = { version = "1.13", optional = true } chacha20 = { version = "0.9", default-features = false } k256 = { version = "0.13.3", optional = true } @@ -14,6 +14,9 @@ base58 = { version = "0.2.0", optional = true } anyhow = { version = "1.0.98", optional = true } borsh = "1.5.7" +[dev-dependencies] +serde_json.workspace = true + [features] default = [] -host = ["thiserror", "bytemuck", "k256", "base58", "anyhow"] +host = ["bytemuck", "k256", "base58", "anyhow"] diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index f32d05d..89bec37 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -4,12 +4,14 @@ use std::{fmt::Display, str::FromStr}; #[cfg(feature = "host")] use base58::{FromBase58, ToBase58}; use borsh::{BorshDeserialize, BorshSerialize}; +pub use data::Data; use serde::{Deserialize, Serialize}; use crate::program::ProgramId; +pub mod data; + pub type Nonce = u128; -pub type Data = Vec; /// Account to be used both in public and private contexts #[derive( @@ -139,7 +141,10 @@ mod tests { let account = Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 1337, - data: b"testing_account_with_metadata_constructor".to_vec(), + data: b"testing_account_with_metadata_constructor" + .to_vec() + .try_into() + .unwrap(), nonce: 0xdeadbeef, }; let fingerprint = AccountId::new([8; 32]); diff --git a/nssa/core/src/account/data.rs b/nssa/core/src/account/data.rs new file mode 100644 index 0000000..281599c --- /dev/null +++ b/nssa/core/src/account/data.rs @@ -0,0 +1,175 @@ +use std::ops::Deref; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "host")] +use crate::error::NssaCoreError; + +pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB + +#[derive(Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug))] +pub struct Data(Vec); + +impl Data { + pub fn into_inner(self) -> Vec { + self.0 + } + + #[cfg(feature = "host")] + pub fn from_cursor(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + use std::io::Read as _; + + let mut u32_bytes = [0u8; 4]; + cursor.read_exact(&mut u32_bytes)?; + let data_length = u32::from_le_bytes(u32_bytes); + if data_length as usize > DATA_MAX_LENGTH_IN_BYTES { + return Err( + std::io::Error::new(std::io::ErrorKind::InvalidData, DataTooBigError).into(), + ); + } + + let mut data = vec![0; data_length as usize]; + cursor.read_exact(&mut data)?; + Ok(Self(data)) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("data length exceeds maximum allowed length of {DATA_MAX_LENGTH_IN_BYTES} bytes")] +pub struct DataTooBigError; + +impl From for Vec { + fn from(data: Data) -> Self { + data.0 + } +} + +impl TryFrom> for Data { + type Error = DataTooBigError; + + fn try_from(value: Vec) -> Result { + if value.len() > DATA_MAX_LENGTH_IN_BYTES { + Err(DataTooBigError) + } else { + Ok(Self(value)) + } + } +} + +impl Deref for Data { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for Data { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'de> Deserialize<'de> for Data { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + /// Data deserialization visitor. + /// + /// Compared to a simple deserialization into a `Vec`, this visitor enforces + /// early length check defined by [`DATA_MAX_LENGTH_IN_BYTES`]. + struct DataVisitor; + + impl<'de> serde::de::Visitor<'de> for DataVisitor { + type Value = Data; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + formatter, + "a byte array with length not exceeding {} bytes", + DATA_MAX_LENGTH_IN_BYTES + ) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut vec = + Vec::with_capacity(seq.size_hint().unwrap_or(0).min(DATA_MAX_LENGTH_IN_BYTES)); + + while let Some(value) = seq.next_element()? { + if vec.len() >= DATA_MAX_LENGTH_IN_BYTES { + return Err(serde::de::Error::custom(DataTooBigError)); + } + vec.push(value); + } + + Ok(Data(vec)) + } + } + + deserializer.deserialize_seq(DataVisitor) + } +} + +impl BorshDeserialize for Data { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + // Implementation adapted from `impl BorshDeserialize for Vec` + + let len = u32::deserialize_reader(reader)?; + match len { + 0 => Ok(Self::default()), + len if len as usize > DATA_MAX_LENGTH_IN_BYTES => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + DataTooBigError, + )), + len => { + let vec_bytes = u8::vec_from_reader(len, reader)? + .expect("can't be None in current borsh crate implementation"); + Ok(Self(vec_bytes)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_data_max_length_allowed() { + let max_vec = vec![0u8; DATA_MAX_LENGTH_IN_BYTES]; + let result = Data::try_from(max_vec); + assert!(result.is_ok()); + } + + #[test] + fn test_data_too_big_error() { + let big_vec = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1]; + let result = Data::try_from(big_vec); + assert!(matches!(result, Err(DataTooBigError))); + } + + #[test] + fn test_borsh_deserialize_exceeding_limit_error() { + let too_big_data = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1]; + let mut serialized = Vec::new(); + <_ as BorshSerialize>::serialize(&too_big_data, &mut serialized).unwrap(); + + let result = ::deserialize(&mut serialized.as_ref()); + assert!(result.is_err()); + } + + #[test] + fn test_json_deserialize_exceeding_limit_error() { + let data = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1]; + let json = serde_json::to_string(&data).unwrap(); + + let result: Result = serde_json::from_str(&json); + assert!(result.is_err()); + } +} diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index e1afe10..5bf620e 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -54,7 +54,7 @@ mod tests { Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 12345678901234567890, - data: b"test data".to_vec(), + data: b"test data".to_vec().try_into().unwrap(), nonce: 18446744073709551614, }, true, @@ -64,7 +64,7 @@ mod tests { Account { program_owner: [9, 9, 9, 8, 8, 8, 7, 7], balance: 123123123456456567112, - data: b"test data".to_vec(), + data: b"test data".to_vec().try_into().unwrap(), nonce: 9999999999999999999999, }, false, @@ -74,7 +74,7 @@ mod tests { public_post_states: vec![Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 100, - data: b"post state data".to_vec(), + data: b"post state data".to_vec().try_into().unwrap(), nonce: 18446744073709551615, }], ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])], diff --git a/nssa/core/src/encoding.rs b/nssa/core/src/encoding.rs index 3a8a128..24ac050 100644 --- a/nssa/core/src/encoding.rs +++ b/nssa/core/src/encoding.rs @@ -26,12 +26,14 @@ impl Account { bytes.extend_from_slice(&self.nonce.to_le_bytes()); let data_length: u32 = self.data.len() as u32; bytes.extend_from_slice(&data_length.to_le_bytes()); - bytes.extend_from_slice(self.data.as_slice()); + bytes.extend_from_slice(self.data.as_ref()); bytes } #[cfg(feature = "host")] pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { + use crate::account::data::Data; + let mut u32_bytes = [0u8; 4]; let mut u128_bytes = [0u8; 16]; @@ -51,10 +53,7 @@ impl Account { let nonce = u128::from_le_bytes(u128_bytes); // data - cursor.read_exact(&mut u32_bytes)?; - let data_length = u32::from_le_bytes(u32_bytes); - let mut data = vec![0; data_length as usize]; - cursor.read_exact(&mut data)?; + let data = Data::from_cursor(cursor)?; Ok(Self { program_owner, @@ -149,7 +148,7 @@ mod tests { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 123456789012345678901234567890123456, nonce: 42, - data: b"hola mundo".to_vec(), + data: b"hola mundo".to_vec().try_into().unwrap(), }; // program owner || balance || nonce || data_len || data @@ -210,7 +209,7 @@ mod tests { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 123456789012345678901234567890123456, nonce: 42, - data: b"hola mundo".to_vec(), + data: b"hola mundo".to_vec().try_into().unwrap(), }; let bytes = account.to_bytes(); let mut cursor = Cursor::new(bytes.as_ref()); diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 04e91c1..8f49724 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -260,7 +260,7 @@ mod tests { let account = Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 1337, - data: vec![0xde, 0xad, 0xbe, 0xef], + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), nonce: 10, }; @@ -275,7 +275,7 @@ mod tests { let account = Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 1337, - data: vec![0xde, 0xad, 0xbe, 0xef], + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), nonce: 10, }; @@ -290,7 +290,7 @@ mod tests { let mut account = Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 1337, - data: vec![0xde, 0xad, 0xbe, 0xef], + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), nonce: 10, }; diff --git a/nssa/program_methods/guest/Cargo.lock b/nssa/program_methods/guest/Cargo.lock index 563e8b9..2c293ec 100644 --- a/nssa/program_methods/guest/Cargo.lock +++ b/nssa/program_methods/guest/Cargo.lock @@ -1578,6 +1578,7 @@ dependencies = [ "chacha20", "risc0-zkvm", "serde", + "thiserror", ] [[package]] diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index 50aac7b..1c880e2 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -63,7 +63,11 @@ fn main() { let mut pinata_post = pinata.account.clone(); let mut winner_post = winner.account.clone(); pinata_post.balance -= PRIZE; - pinata_post.data = data.next_data().to_vec(); + pinata_post.data = data + .next_data() + .to_vec() + .try_into() + .expect("33 bytes should fit into Data"); winner_post.balance += PRIZE; write_nssa_outputs( diff --git a/nssa/program_methods/guest/src/bin/pinata_token.rs b/nssa/program_methods/guest/src/bin/pinata_token.rs index 91d887a..3810485 100644 --- a/nssa/program_methods/guest/src/bin/pinata_token.rs +++ b/nssa/program_methods/guest/src/bin/pinata_token.rs @@ -1,6 +1,9 @@ -use nssa_core::program::{ - AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs, - write_nssa_outputs_with_chained_call, +use nssa_core::{ + account::Data, + program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, + }, }; use risc0_zkvm::{ serde::to_vec, @@ -38,11 +41,11 @@ impl Challenge { digest[..difficulty].iter().all(|&b| b == 0) } - fn next_data(self) -> [u8; 33] { + fn next_data(self) -> Data { let mut result = [0; 33]; result[0] = self.difficulty; result[1..].copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes()); - result + result.to_vec().try_into().expect("should fit") } } @@ -74,7 +77,7 @@ fn main() { let mut pinata_definition_post = pinata_definition.account.clone(); let pinata_token_holding_post = pinata_token_holding.account.clone(); let winner_token_holding_post = winner_token_holding.account.clone(); - pinata_definition_post.data = data.next_data().to_vec(); + pinata_definition_post.data = data.next_data(); let mut instruction_data: [u8; 23] = [0; 23]; instruction_data[0] = 1; diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 5ac3f97..614b5d9 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -1,5 +1,5 @@ use nssa_core::{ - account::{Account, AccountId, AccountWithMetadata, Data}, + account::{Account, AccountId, AccountWithMetadata, Data, data::DATA_MAX_LENGTH_IN_BYTES}, program::{ AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, }, @@ -25,9 +25,11 @@ use nssa_core::{ const TOKEN_DEFINITION_TYPE: u8 = 0; const TOKEN_DEFINITION_DATA_SIZE: usize = 23; +const _: () = assert!(TOKEN_DEFINITION_DATA_SIZE <= DATA_MAX_LENGTH_IN_BYTES); const TOKEN_HOLDING_TYPE: u8 = 1; const TOKEN_HOLDING_DATA_SIZE: usize = 49; +const _: () = assert!(TOKEN_HOLDING_DATA_SIZE <= DATA_MAX_LENGTH_IN_BYTES); struct TokenDefinition { account_type: u8, @@ -42,12 +44,15 @@ struct TokenHolding { } impl TokenDefinition { - fn into_data(self) -> Vec { + fn into_data(self) -> Data { 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()); - bytes.into() + bytes + .to_vec() + .try_into() + .expect("23 bytes should fit into Data") } fn parse(data: &[u8]) -> Option { @@ -107,7 +112,10 @@ impl TokenHolding { 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() + bytes + .to_vec() + .try_into() + .expect("33 bytes should fit into Data") } } @@ -398,15 +406,15 @@ mod tests { 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.account().data, - vec![ + definition_account.account().data.as_ref(), + &[ 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.account().data, - vec![ + holding_account.account().data.as_ref(), + &[ 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 @@ -456,7 +464,9 @@ mod tests { AccountWithMetadata { account: Account { // First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts - data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE], + data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE] + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -478,7 +488,7 @@ mod tests { AccountWithMetadata { account: Account { // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1], + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -500,7 +510,7 @@ mod tests { AccountWithMetadata { account: Account { // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1], + data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -521,7 +531,7 @@ mod tests { let pre_states = vec![ AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -529,10 +539,12 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1] + data: [1] .into_iter() .chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1]) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -549,10 +561,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -560,7 +574,7 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -578,10 +592,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: false, @@ -589,7 +605,7 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -605,10 +621,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -617,10 +635,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 255 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(255)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -630,15 +650,15 @@ mod tests { let post_states = transfer(&pre_states, 11); let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); assert_eq!( - sender_post.account().data, - vec![ + sender_post.account().data.as_ref(), + [ 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.account().data, - vec![ + recipient_post.account().data.as_ref(), + [ 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 ] @@ -654,7 +674,9 @@ mod tests { data: [0; TOKEN_DEFINITION_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(1000)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: false, @@ -668,10 +690,13 @@ mod tests { ]; let post_states = initialize_account(&pre_states); let [definition, holding] = post_states.try_into().ok().unwrap(); - assert_eq!(definition.account().data, pre_states[0].account.data); assert_eq!( - holding.account().data, - vec![ + definition.account().data.as_ref(), + pre_states[0].account.data.as_ref() + ); + assert_eq!( + holding.account().data.as_ref(), + [ 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index eeba692..4ef02b3 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -95,7 +95,7 @@ impl Proof { mod tests { use nssa_core::{ Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, - account::{Account, AccountId, AccountWithMetadata}, + account::{Account, AccountId, AccountWithMetadata, data::Data}, }; use super::*; @@ -134,14 +134,14 @@ mod tests { program_owner: program.id(), balance: 100 - balance_to_move, nonce: 1, - data: vec![], + data: Data::default(), }; let expected_recipient_post = Account { program_owner: program.id(), balance: balance_to_move, nonce: 0xdeadbeef, - data: vec![], + data: Data::default(), }; let expected_sender_pre = sender.clone(); @@ -191,7 +191,7 @@ mod tests { balance: 100, nonce: 0xdeadbeef, program_owner: program.id(), - data: vec![], + data: Data::default(), }, true, AccountId::from(&sender_keys.npk()), diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 9359b04..5841f78 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -234,7 +234,7 @@ impl V02State { program_owner: Program::pinata().id(), balance: 1500, // Difficulty: 3 - data: vec![3; 33], + data: vec![3; 33].try_into().unwrap(), nonce: 0, }, ); @@ -248,7 +248,7 @@ impl V02State { Account { program_owner: Program::pinata_token().id(), // Difficulty: 3 - data: vec![3; 33], + data: vec![3; 33].try_into().expect("should fit"), ..Account::default() }, ); @@ -262,7 +262,7 @@ pub mod tests { use nssa_core::{ Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - account::{Account, AccountId, AccountWithMetadata, Nonce}, + account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar}, program::{PdaSeed, ProgramId}, }; @@ -505,7 +505,7 @@ pub mod tests { ..Account::default() }; let account_with_default_values_except_data = Account { - data: vec![0xca, 0xfe], + data: vec![0xca, 0xfe].try_into().unwrap(), ..Account::default() }; self.force_insert_account( @@ -1027,7 +1027,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), balance: 100, nonce: 0xdeadbeef, - data: vec![], + data: Data::default(), }; let recipient_keys = test_private_account_keys_2(); @@ -1051,7 +1051,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), nonce: 0xcafecafe, balance: sender_private_account.balance - balance_to_move, - data: vec![], + data: Data::default(), }, ); @@ -1093,7 +1093,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), balance: 100, nonce: 0xdeadbeef, - data: vec![], + data: Data::default(), }; let recipient_keys = test_public_account_keys_1(); let recipient_initial_balance = 400; @@ -1126,7 +1126,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), nonce: 0xcafecafe, balance: sender_private_account.balance - balance_to_move, - data: vec![], + data: Data::default(), }, ); @@ -1692,7 +1692,7 @@ pub mod tests { let private_account_2 = AccountWithMetadata::new( Account { // Non default data - data: b"hola mundo".to_vec(), + data: b"hola mundo".to_vec().try_into().unwrap(), ..Account::default() }, false, @@ -1981,7 +1981,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), balance: 100, nonce: 0xdeadbeef, - data: vec![], + data: Data::default(), }; let recipient_keys = test_private_account_keys_2(); @@ -2007,7 +2007,7 @@ pub mod tests { program_owner: Program::authenticated_transfer_program().id(), balance: 100 - balance_to_move, nonce: 0xcafecafe, - data: vec![], + data: Data::default(), }; let tx = private_balance_transfer_for_tests( @@ -2298,7 +2298,7 @@ pub mod tests { expected_winner_account_data[33..].copy_from_slice(&150u128.to_le_bytes()); let expected_winner_token_holding_post = Account { program_owner: token.id(), - data: expected_winner_account_data.to_vec(), + data: expected_winner_account_data.to_vec().try_into().unwrap(), ..Account::default() }; diff --git a/nssa/test_program_methods/guest/Cargo.lock b/nssa/test_program_methods/guest/Cargo.lock index 85f566c..b2337cc 100644 --- a/nssa/test_program_methods/guest/Cargo.lock +++ b/nssa/test_program_methods/guest/Cargo.lock @@ -1583,6 +1583,7 @@ dependencies = [ "chacha20", "risc0-zkvm", "serde", + "thiserror", ] [[package]] diff --git a/nssa/test_program_methods/guest/src/bin/data_changer.rs b/nssa/test_program_methods/guest/src/bin/data_changer.rs index 2869d01..16c2359 100644 --- a/nssa/test_program_methods/guest/src/bin/data_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/data_changer.rs @@ -12,7 +12,9 @@ fn main() { let account_pre = &pre.account; let mut account_post = account_pre.clone(); - account_post.data.push(0); + let mut data_vec = account_post.data.into_inner(); + data_vec.push(0); + account_post.data = data_vec.try_into().expect("data_vec should fit into Data"); write_nssa_outputs(vec![pre], vec![AccountPostState::new_claimed(account_post)]); } diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index c0e2223..7712a7c 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -197,6 +197,7 @@ async fn find_solution(wallet: &WalletCore, pinata_account_id: nssa::AccountId) let account = wallet.get_account_public(pinata_account_id).await?; let data: [u8; 33] = account .data + .as_ref() .try_into() .map_err(|_| anyhow::Error::msg("invalid pinata account data"))?; From 31e70169481a46630303cba8575f729e67c04987 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Fri, 5 Dec 2025 17:16:41 +0300 Subject: [PATCH 52/70] feat: introduce parameter to data_changer and add test for account data --- integration_tests/src/data_changer.bin | Bin 371256 -> 377792 bytes integration_tests/src/test_suite_map.rs | 2 +- nssa/core/Cargo.toml | 4 +- nssa/core/src/account/data.rs | 9 ++--- nssa/src/state.rs | 35 ++++++++++++++++-- .../guest/src/bin/data_changer.rs | 9 ++--- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/integration_tests/src/data_changer.bin b/integration_tests/src/data_changer.bin index 6a36d52c37bd34dcb6e4b70f7fedc9f49ecff445..3d062c300fa1a9e6a5cc85e9888e5abaed93d751 100644 GIT binary patch delta 119262 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 z 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 a^{3(^yxQ&BpK7HqE?;}V^ugc1^#1`})LY>I diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 17caf26..7ee3ef4 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1466,7 +1466,7 @@ pub fn prepare_function_map() -> HashMap { data_changer.id(), vec![account_id], vec![], - (), + vec![0], ) .unwrap(); let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index f179899..80fe7df 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -15,8 +15,8 @@ anyhow = { version = "1.0.98", optional = true } borsh = "1.5.7" [dev-dependencies] -serde_json.workspace = true +serde_json = "1.0.81" [features] default = [] -host = ["bytemuck", "k256", "base58", "anyhow"] +host = ["dep:bytemuck", "dep:k256", "dep:base58", "dep:anyhow"] diff --git a/nssa/core/src/account/data.rs b/nssa/core/src/account/data.rs index 281599c..974cb06 100644 --- a/nssa/core/src/account/data.rs +++ b/nssa/core/src/account/data.rs @@ -3,9 +3,6 @@ use std::ops::Deref; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -#[cfg(feature = "host")] -use crate::error::NssaCoreError; - pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB #[derive(Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)] @@ -18,7 +15,9 @@ impl Data { } #[cfg(feature = "host")] - pub fn from_cursor(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + pub fn from_cursor( + cursor: &mut std::io::Cursor<&[u8]>, + ) -> Result { use std::io::Read as _; let mut u32_bytes = [0u8; 4]; @@ -36,7 +35,7 @@ impl Data { } } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] #[error("data length exceeds maximum allowed length of {DATA_MAX_LENGTH_IN_BYTES} bytes")] pub struct DataTooBigError; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 5841f78..72efbd2 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -234,7 +234,7 @@ impl V02State { program_owner: Program::pinata().id(), balance: 1500, // Difficulty: 3 - data: vec![3; 33].try_into().unwrap(), + data: vec![3; 33].try_into().expect("should fit"), nonce: 0, }, ); @@ -730,7 +730,8 @@ pub mod tests { program_id ); let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); + public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0]) + .unwrap(); let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); let tx = PublicTransaction::new(message, witness_set); @@ -1248,7 +1249,7 @@ pub mod tests { let result = execute_and_prove( &[public_account], - &Program::serialize_instruction(()).unwrap(), + &Program::serialize_instruction(vec![0]).unwrap(), &[0], &[], &[], @@ -1259,6 +1260,34 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } + #[test] + fn test_data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_circuit() { + let program = Program::data_changer(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let large_data: Vec = vec![0; nssa_core::account::data::DATA_MAX_LENGTH_IN_BYTES + 1]; + + let result = execute_and_prove( + &[public_account], + &Program::serialize_instruction(large_data).unwrap(), + &[0], + &[], + &[], + &[], + &program, + ); + + assert!(matches!(result, Err(NssaError::ProgramProveFailed(_)))); + } + #[test] fn test_extra_output_program_should_fail_in_privacy_preserving_circuit() { let program = Program::extra_output_program(); diff --git a/nssa/test_program_methods/guest/src/bin/data_changer.rs b/nssa/test_program_methods/guest/src/bin/data_changer.rs index 16c2359..b590886 100644 --- a/nssa/test_program_methods/guest/src/bin/data_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/data_changer.rs @@ -1,9 +1,10 @@ use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; -type Instruction = (); +type Instruction = Vec; +/// A program that modifies the account data by setting bytes sent in instruction. fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let ProgramInput { pre_states, instruction: data } = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -12,9 +13,7 @@ fn main() { let account_pre = &pre.account; let mut account_post = account_pre.clone(); - let mut data_vec = account_post.data.into_inner(); - data_vec.push(0); - account_post.data = data_vec.try_into().expect("data_vec should fit into Data"); + account_post.data = data.try_into().expect("provided data should fit into data limit"); write_nssa_outputs(vec![pre], vec![AccountPostState::new_claimed(account_post)]); } From 8cfb586bee7430424228616faf1afcc9d753646b Mon Sep 17 00:00:00 2001 From: Sergio Chouhy <41742639+schouhy@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:55:12 -0300 Subject: [PATCH 53/70] Update wallet/src/cli/mod.rs Co-authored-by: Daniil Polyakov --- wallet/src/cli/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 69d7ccb..1b4c779 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -165,7 +165,7 @@ pub async fn execute_subcommand(command: Command) -> Result Date: Tue, 9 Dec 2025 15:18:48 -0300 Subject: [PATCH 54/70] use pathbuf and context --- integration_tests/src/test_suite_map.rs | 2 +- wallet/src/cli/mod.rs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 5c9b46c..c7116fe 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1442,7 +1442,7 @@ pub fn prepare_function_map() -> HashMap { pub async fn test_program_deployment() { info!("########## test program deployment ##########"); - let binary_filepath = NSSA_PROGRAM_FOR_TEST_DATA_CHANGER.to_string(); + let binary_filepath: PathBuf = NSSA_PROGRAM_FOR_TEST_DATA_CHANGER.parse().unwrap(); let command = Command::DeployProgram { binary_filepath: binary_filepath.clone(), diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 69d7ccb..741f280 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,4 +1,6 @@ -use anyhow::Result; +use std::path::PathBuf; + +use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use nssa::{ProgramDeploymentTransaction, program::Program}; @@ -52,7 +54,7 @@ pub enum Command { #[command(subcommand)] Config(ConfigSubcommand), /// Deploy a program - DeployProgram { binary_filepath: String }, + DeployProgram { binary_filepath: PathBuf }, } /// Represents overarching CLI command for a wallet with setup included @@ -157,14 +159,19 @@ pub async fn execute_subcommand(command: Command) -> Result { - let bytecode: Vec = std::fs::read(binary_filepath).expect("File not found"); + let bytecode: Vec = std::fs::read(&binary_filepath).with_context(|| { + format!( + "Failed to read program binary at {}", + binary_filepath.display() + ) + })?; let message = nssa::program_deployment_transaction::Message::new(bytecode); let transaction = ProgramDeploymentTransaction::new(message); let response = wallet_core .sequencer_client .send_tx_program(transaction) .await - .expect("Transaction submission error"); + .with_context(|| "Transaction submission error"); println!("Response: {:?}", response); SubcommandReturnValue::Empty From 17f77f9ae788946edad66184e9ee82784a1bc566 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:31:02 -0500 Subject: [PATCH 55/70] Update nssa/program_methods/guest/src/bin/token.rs Co-authored-by: Daniil Polyakov --- nssa/program_methods/guest/src/bin/token.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index a5aed8b..bc985cf 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -25,7 +25,7 @@ use nssa_core::{ // * Two accounts: [definition_account, account_to_initialize]. // * An dummy byte string of length 23, with the following layout // [0x02 || 0x00 || 0x00 || 0x00 || ... || 0x00 || 0x00]. -// 4. Burn tokens from a Toking Holding account (thus lowering total supply) +// 4. Burn tokens from a Token Holding account (thus lowering total supply) // Arguments to this function are: // * Two accounts: [definition_account, holding_account]. // * An instruction data byte string of length 23, indicating the balance to burn with the folloiwng layout From cf4d7ba80b04b2e3e0a2210cbd68992e44286cb5 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:31:13 -0500 Subject: [PATCH 56/70] Update nssa/program_methods/guest/src/bin/token.rs Co-authored-by: Daniil Polyakov --- nssa/program_methods/guest/src/bin/token.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index bc985cf..50c5d99 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -30,7 +30,7 @@ use nssa_core::{ // * Two accounts: [definition_account, holding_account]. // * An instruction data byte string of length 23, indicating the balance to burn with the folloiwng layout // [0x03 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. -// 5. Mint additional supply of tokens tokens to a Toking Holding account (thus increasing total supply) +// 5. Mint additional supply of tokens tokens to a Token Holding account (thus increasing total supply) // Arguments to this function are: // * Two accounts: [definition_account, holding_account]. // * An instruction data byte string of length 23, indicating the balance to mint with the folloiwng layout From 8c493a015515bfe6177bcca99f8b5d20246ecb4d Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:31:44 -0500 Subject: [PATCH 57/70] Update nssa/program_methods/guest/src/bin/token.rs Co-authored-by: Daniil Polyakov --- nssa/program_methods/guest/src/bin/token.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 50c5d99..2127f65 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -335,7 +335,7 @@ fn mint_additional_supply(pre_states: &[AccountWithMetadata], amount_to_mint: u1 let post_total_supply = definition_values .total_supply .checked_add(amount_to_mint) - .expect("Total supply overflow."); + .expect("Total supply overflow"); let post_definition_data = TokenDefinition { account_type: definition_values.account_type, From 70c228dbecb9f9814eebff1b736ae28f7f213a2e Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:28:32 -0500 Subject: [PATCH 58/70] fixed formatting and added overflow/underflow --- nssa/program_methods/guest/src/bin/token.rs | 448 ++++++++++++-------- 1 file changed, 272 insertions(+), 176 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 2127f65..4914b4a 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -28,11 +28,13 @@ use nssa_core::{ // 4. Burn tokens from a Token Holding account (thus lowering total supply) // Arguments to this function are: // * Two accounts: [definition_account, holding_account]. +// * Authorization required: holding_account // * An instruction data byte string of length 23, indicating the balance to burn with the folloiwng layout // [0x03 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. // 5. Mint additional supply of tokens tokens to a Token Holding account (thus increasing total supply) // Arguments to this function are: // * Two accounts: [definition_account, holding_account]. +// * Authorization required: definition_account // * An instruction data byte string of length 23, indicating the balance to mint with the folloiwng layout // [0x04 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. @@ -155,7 +157,7 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec Vec Vec { - if pre_states.len() != 2 { panic!("Invalid number of accounts"); } @@ -262,8 +263,8 @@ fn burn(pre_states: &[AccountWithMetadata], balance_to_burn: u128) -> Vec Vec Vec Vec { +fn mint_additional_supply( + pre_states: &[AccountWithMetadata], + amount_to_mint: u128, +) -> Vec { if pre_states.len() != 2 { panic!("Invalid number of accounts"); } - + let definition = &pre_states[0]; let token_holding = &pre_states[1]; @@ -320,27 +322,32 @@ fn mint_additional_supply(pre_states: &[AccountWithMetadata], amount_to_mint: u1 let token_holding_values: TokenHolding = if token_holding.account == Account::default() { TokenHolding::new(&definition.account_id) - } else { TokenHolding::parse(&token_holding.account.data).expect("Holding account must be valid") }; + } else { + TokenHolding::parse(&token_holding.account.data).expect("Holding account must be valid") + }; if definition.account_id != token_holding_values.definition_id { panic!("Mismatch token definition and token holding"); } let token_holding_post_data = TokenHolding { - account_type: token_holding_values.account_type, - definition_id: token_holding_values.definition_id, - balance: token_holding_values.balance + amount_to_mint, + account_type: token_holding_values.account_type, + definition_id: token_holding_values.definition_id, + balance: token_holding_values + .balance + .checked_add(amount_to_mint) + .expect("New balance overflow"), }; let post_total_supply = definition_values - .total_supply - .checked_add(amount_to_mint) - .expect("Total supply overflow"); + .total_supply + .checked_add(amount_to_mint) + .expect("Total supply overflow"); let post_definition_data = TokenDefinition { - account_type: definition_values.account_type, - name: definition_values.name, - total_supply: post_total_supply, + account_type: definition_values.account_type, + name: definition_values.name, + total_supply: post_total_supply, }; let post_definition = { @@ -363,7 +370,6 @@ fn mint_additional_supply(pre_states: &[AccountWithMetadata], amount_to_mint: u1 vec![post_definition, token_holding_post] } - type Instruction = [u8; 23]; fn main() { @@ -449,9 +455,9 @@ mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; use crate::{ - TOKEN_DEFINITION_DATA_SIZE, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE, - TOKEN_DEFINITION_TYPE, TokenDefinition, TokenHolding, - initialize_account, new_definition, transfer, burn, mint_additional_supply, + TOKEN_DEFINITION_DATA_SIZE, TOKEN_DEFINITION_TYPE, TOKEN_HOLDING_DATA_SIZE, + TOKEN_HOLDING_TYPE, TokenDefinition, TokenHolding, burn, initialize_account, + mint_additional_supply, new_definition, transfer, }; #[should_panic(expected = "Invalid number of input accounts")] @@ -852,12 +858,14 @@ mod tests { HoldingDiffDef, HoldingSameDefAuth, HoldingSameDefNotAuth, + HoldingSameDefNotAuthOverflow, DefinitionAccountPostBurn, HoldingAccountPostBurn, Uninit, InitMint, DefinitionAccountMint, HoldingSameDefMint, + HoldingSameDefAuthLargeBalance, } enum IdEnum { @@ -866,49 +874,45 @@ mod tests { HoldingId, } - fn helper_account_constructor(selection: AccountsEnum) -> AccountWithMetadata{ + fn helper_account_constructor(selection: AccountsEnum) -> AccountWithMetadata { match selection { AccountsEnum::DefinitionAccountAuth => AccountWithMetadata { account: Account { - program_owner: [5u32;8], - balance: 0u128, - data: TokenDefinition::into_data( - TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, - name: [2; 6], - total_supply: helper_balance_constructor(BalanceEnum::InitSupply), - }), - nonce: 0, + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name: [2; 6], + total_supply: helper_balance_constructor(BalanceEnum::InitSupply), + }), + nonce: 0, }, is_authorized: true, account_id: helper_id_constructor(IdEnum::PoolDefinitionId), }, AccountsEnum::DefinitionAccountNotAuth => AccountWithMetadata { account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data( - TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, - name: [2; 6], - total_supply: helper_balance_constructor(BalanceEnum::InitSupply), - }), - nonce: 0, + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name: [2; 6], + total_supply: helper_balance_constructor(BalanceEnum::InitSupply), + }), + nonce: 0, }, is_authorized: false, account_id: helper_id_constructor(IdEnum::PoolDefinitionId), }, AccountsEnum::HoldingDiffDef => AccountWithMetadata { account: Account { - program_owner: [5u32;8], + program_owner: [5u32; 8], balance: 0u128, - data: TokenHolding::into_data( - TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionIdDiff), - balance: helper_balance_constructor(BalanceEnum::HoldingBalance), - } - ), + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionIdDiff), + balance: helper_balance_constructor(BalanceEnum::HoldingBalance), + }), nonce: 0, }, is_authorized: true, @@ -916,15 +920,13 @@ mod tests { }, AccountsEnum::HoldingSameDefAuth => AccountWithMetadata { account: Account { - program_owner: [5u32;8], + program_owner: [5u32; 8], balance: 0u128, - data: TokenHolding::into_data( - TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::HoldingBalance), - } - ), + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::HoldingBalance), + }), nonce: 0, }, is_authorized: true, @@ -932,15 +934,27 @@ mod tests { }, AccountsEnum::HoldingSameDefNotAuth => AccountWithMetadata { account: Account { - program_owner: [5u32;8], + program_owner: [5u32; 8], balance: 0u128, - data: TokenHolding::into_data( - TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::HoldingBalance), - } - ), + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::HoldingBalance), + }), + nonce: 0, + }, + is_authorized: false, + account_id: helper_id_constructor(IdEnum::HoldingId), + }, + AccountsEnum::HoldingSameDefNotAuthOverflow => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::InitSupply), + }), nonce: 0, }, is_authorized: false, @@ -948,30 +962,27 @@ mod tests { }, AccountsEnum::DefinitionAccountPostBurn => AccountWithMetadata { account: Account { - program_owner: [5u32;8], - balance: 0u128, - data: TokenDefinition::into_data( - TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, - name: [2; 6], - total_supply: helper_balance_constructor(BalanceEnum::InitSupplyBurned), - }), - nonce: 0, + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name: [2; 6], + total_supply: helper_balance_constructor(BalanceEnum::InitSupplyBurned), + }), + nonce: 0, }, is_authorized: true, account_id: helper_id_constructor(IdEnum::PoolDefinitionId), }, AccountsEnum::HoldingAccountPostBurn => AccountWithMetadata { account: Account { - program_owner: [5u32;8], + program_owner: [5u32; 8], balance: 0u128, - data: TokenHolding::into_data( - TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::HoldingBalanceBurned), - } - ), + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::HoldingBalanceBurned), + }), nonce: 0, }, is_authorized: false, @@ -984,15 +995,13 @@ mod tests { }, AccountsEnum::InitMint => AccountWithMetadata { account: Account { - program_owner: [0u32;8], + program_owner: [0u32; 8], balance: 0u128, - data: TokenHolding::into_data( - TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::MintSuccess), - } - ), + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::MintSuccess), + }), nonce: 0, }, is_authorized: false, @@ -1000,36 +1009,47 @@ mod tests { }, AccountsEnum::HoldingSameDefMint => AccountWithMetadata { account: Account { - program_owner: [5u32;8], - balance: 0u128, - data: TokenHolding::into_data( - TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::HoldingBalanceMint), - } - ), - nonce: 0, + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::HoldingBalanceMint), + }), + nonce: 0, }, is_authorized: true, account_id: helper_id_constructor(IdEnum::PoolDefinitionId), }, AccountsEnum::DefinitionAccountMint => AccountWithMetadata { account: Account { - program_owner: [5u32;8], - balance: 0u128, - data: TokenDefinition::into_data( - TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, - name: [2; 6], - total_supply: helper_balance_constructor(BalanceEnum::InitSupplyMint), - }), - nonce: 0, + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name: [2; 6], + total_supply: helper_balance_constructor(BalanceEnum::InitSupplyMint), + }), + nonce: 0, }, is_authorized: true, account_id: helper_id_constructor(IdEnum::PoolDefinitionId), }, - _ => panic!("Invalid selection") + AccountsEnum::HoldingSameDefAuthLargeBalance => AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), + balance: helper_balance_constructor(BalanceEnum::MintOverflow), + }), + nonce: 0, + }, + is_authorized: true, + account_id: helper_id_constructor(IdEnum::PoolDefinitionId), + }, + _ => panic!("Invalid selection"), } } @@ -1045,152 +1065,228 @@ mod tests { BalanceEnum::InitSupplyMint => 150_000, BalanceEnum::HoldingBalanceMint => 51_000, BalanceEnum::MintOverflow => (2 as u128).pow(128) - 40_000, - _ => panic!("Invalid selection") + _ => panic!("Invalid selection"), } } fn helper_id_constructor(selection: IdEnum) -> AccountId { match selection { - IdEnum::PoolDefinitionId => AccountId::new([15;32]), - IdEnum::PoolDefinitionIdDiff => AccountId::new([16;32]), - IdEnum::HoldingId => AccountId::new([17;32]), + IdEnum::PoolDefinitionId => AccountId::new([15; 32]), + IdEnum::PoolDefinitionIdDiff => AccountId::new([16; 32]), + IdEnum::HoldingId => AccountId::new([17; 32]), } } #[test] #[should_panic(expected = "Invalid number of accounts")] fn test_burn_invalid_number_of_accounts() { - let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - ]; - let _post_states = burn(&pre_states, helper_balance_constructor(BalanceEnum::BurnSuccess)); + let pre_states = vec![helper_account_constructor( + AccountsEnum::DefinitionAccountAuth, + )]; + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnSuccess), + ); } #[test] #[should_panic(expected = "Mismatch token definition and token holding")] fn test_burn_mismatch_def() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingDiffDef), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingDiffDef), ]; - let _post_states = burn(&pre_states, helper_balance_constructor(BalanceEnum::BurnSuccess)); + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnSuccess), + ); } #[test] #[should_panic(expected = "Authorization is missing")] fn test_burn_missing_authorization() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), ]; - let _post_states = burn(&pre_states, helper_balance_constructor(BalanceEnum::BurnSuccess)); + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnSuccess), + ); } #[test] #[should_panic(expected = "Insufficient balance to burn")] fn test_burn_insufficient_balance() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefAuth), ]; - let _post_states = burn(&pre_states, helper_balance_constructor(BalanceEnum::BurnInsufficient)); + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnInsufficient), + ); + } + + #[test] + #[should_panic(expected = "Total supply underflow")] + fn test_burn_total_supply_underflow() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefAuthLargeBalance), + ]; + let _post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::MintOverflow), + ); } #[test] fn test_burn_success() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefAuth), ]; - let post_states = burn(&pre_states, helper_balance_constructor(BalanceEnum::BurnSuccess)); + let post_states = burn( + &pre_states, + helper_balance_constructor(BalanceEnum::BurnSuccess), + ); let def_post = post_states[0].clone(); let holding_post = post_states[1].clone(); - assert!(*def_post.account() == helper_account_constructor(AccountsEnum::DefinitionAccountPostBurn).account); - assert!(*holding_post.account() == helper_account_constructor(AccountsEnum::HoldingAccountPostBurn).account); + assert!( + *def_post.account() + == helper_account_constructor(AccountsEnum::DefinitionAccountPostBurn).account + ); + assert!( + *holding_post.account() + == helper_account_constructor(AccountsEnum::HoldingAccountPostBurn).account + ); } #[test] #[should_panic(expected = "Invalid number of accounts")] fn test_mint_invalid_number_of_accounts() { - let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - ]; - let _post_states = mint_additional_supply(&pre_states, helper_balance_constructor(BalanceEnum::MintSuccess)); + let pre_states = vec![helper_account_constructor( + AccountsEnum::DefinitionAccountAuth, + )]; + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); } #[test] #[should_panic(expected = "Holding account must be valid")] fn test_mint_not_valid_holding_account() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::DefinitionAccountNotAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountNotAuth), ]; - let _post_states = mint_additional_supply(&pre_states, helper_balance_constructor(BalanceEnum::MintSuccess)); + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); } #[test] #[should_panic(expected = "Definition authorization is missing")] fn test_mint_missing_authorization() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountNotAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountNotAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), ]; - let _post_states = mint_additional_supply(&pre_states, helper_balance_constructor(BalanceEnum::MintSuccess)); + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); } #[test] #[should_panic(expected = "Mismatch token definition and token holding")] fn test_mint_mismatched_token_definition() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingDiffDef), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingDiffDef), ]; - let _post_states = mint_additional_supply(&pre_states, helper_balance_constructor(BalanceEnum::MintSuccess)); + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); } #[test] fn test_mint_success() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), ]; - let post_states = mint_additional_supply(&pre_states, helper_balance_constructor(BalanceEnum::MintSuccess)); + let post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); let def_post = post_states[0].clone(); let holding_post = post_states[1].clone(); - assert!(*def_post.account() == helper_account_constructor(AccountsEnum::DefinitionAccountMint).account); - assert!(*holding_post.account() == helper_account_constructor(AccountsEnum::HoldingSameDefMint).account); + assert!( + *def_post.account() + == helper_account_constructor(AccountsEnum::DefinitionAccountMint).account + ); + assert!( + *holding_post.account() + == helper_account_constructor(AccountsEnum::HoldingSameDefMint).account + ); } #[test] fn test_mint_uninit_holding_success() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::Uninit), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::Uninit), ]; - let post_states = mint_additional_supply(&pre_states, helper_balance_constructor(BalanceEnum::MintSuccess)); + let post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintSuccess), + ); let def_post = post_states[0].clone(); let holding_post = post_states[1].clone(); - assert!(*def_post.account() == helper_account_constructor(AccountsEnum::DefinitionAccountMint).account); - assert!(*holding_post.account() == helper_account_constructor(AccountsEnum::InitMint).account); + assert!( + *def_post.account() + == helper_account_constructor(AccountsEnum::DefinitionAccountMint).account + ); + assert!( + *holding_post.account() == helper_account_constructor(AccountsEnum::InitMint).account + ); assert!(holding_post.requires_claim() == true); } #[test] - #[should_panic(expected = "Total supply overflow.")] - fn test_mint_overflow() { + #[should_panic(expected = "Total supply overflow")] + fn test_mint_total_supply_overflow() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), ]; - let _post_states = mint_additional_supply(&pre_states, - helper_balance_constructor(BalanceEnum::MintOverflow)); - + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintOverflow), + ); } -} \ No newline at end of file + #[test] + #[should_panic(expected = "New balance overflow")] + fn test_mint_holding_account_overflow() { + let pre_states = vec![ + helper_account_constructor(AccountsEnum::DefinitionAccountAuth), + helper_account_constructor(AccountsEnum::HoldingSameDefNotAuthOverflow), + ]; + let _post_states = mint_additional_supply( + &pre_states, + helper_balance_constructor(BalanceEnum::MintOverflow), + ); + } +} From 45e3223d516578744ef7b41336d47df1c97b276b Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 10 Dec 2025 10:25:33 +0200 Subject: [PATCH 59/70] fix: merge fix --- integration_tests/src/test_suite_map.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 1f0b7ec..582a093 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -604,7 +604,7 @@ pub fn prepare_function_map() -> HashMap { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private { - cci: ChainIndex::root(), + cci: Some(ChainIndex::root()), }, ))) .await @@ -617,7 +617,7 @@ pub fn prepare_function_map() -> HashMap { account_id: supply_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Public { - cci: ChainIndex::root(), + cci: Some(ChainIndex::root()), }, ))) .await @@ -666,8 +666,8 @@ pub fn prepare_function_map() -> HashMap { // The data of a token definition account has the following layout: // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ] assert_eq!( - supply_acc.data, - vec![ + supply_acc.data.as_ref(), + &[ 1, 128, 101, 5, 31, 43, 36, 97, 108, 164, 92, 25, 157, 173, 5, 14, 194, 121, 239, 84, 19, 160, 243, 47, 193, 2, 250, 247, 232, 253, 191, 232, 173, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 @@ -689,7 +689,7 @@ pub fn prepare_function_map() -> HashMap { account_id: definition_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private { - cci: ChainIndex::root(), + cci: Some(ChainIndex::root()), }, ))) .await @@ -702,7 +702,7 @@ pub fn prepare_function_map() -> HashMap { account_id: supply_account_id, } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( NewSubcommand::Private { - cci: ChainIndex::root(), + cci: Some(ChainIndex::root()), }, ))) .await @@ -756,8 +756,8 @@ pub fn prepare_function_map() -> HashMap { // 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![ + definition_acc.data.as_ref(), + &[ 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); @@ -766,8 +766,8 @@ pub fn prepare_function_map() -> HashMap { // The data of a token definition account has the following layout: // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ] assert_eq!( - supply_acc.data, - vec![ + supply_acc.data.as_ref(), + &[ 1, 128, 101, 5, 31, 43, 36, 97, 108, 164, 92, 25, 157, 173, 5, 14, 194, 121, 239, 84, 19, 160, 243, 47, 193, 2, 250, 247, 232, 253, 191, 232, 173, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 From 340c9c4ce6509782d4d0341360a9ee0e6af70e43 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Wed, 10 Dec 2025 08:12:17 -0500 Subject: [PATCH 60/70] redundant underflow check added --- nssa/program_methods/guest/src/bin/token.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 4914b4a..8409d76 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -284,7 +284,10 @@ fn burn(pre_states: &[AccountWithMetadata], balance_to_burn: u128) -> Vec Date: Wed, 10 Dec 2025 14:06:48 -0300 Subject: [PATCH 61/70] move modified transfer program to test programs --- nssa/src/program.rs | 15 ++++++++------- .../guest/src/bin/modified_transfer.rs | 0 2 files changed, 8 insertions(+), 7 deletions(-) rename nssa/{program_methods => test_program_methods}/guest/src/bin/modified_transfer.rs (100%) diff --git a/nssa/src/program.rs b/nssa/src/program.rs index f91a007..89c3ed3 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -7,7 +7,7 @@ use serde::Serialize; use crate::{ error::NssaError, - program_methods::{AUTHENTICATED_TRANSFER_ELF, MODIFIED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, + program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, }; /// Maximum number of cycles for a public execution. @@ -95,12 +95,6 @@ impl Program { // `program_methods` Self::new(TOKEN_ELF.to_vec()).unwrap() } - - pub fn modified_transfer_program() -> Self { - // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of - // `program_methods` - Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() - } } // TODO: Testnet only. Refactor to prevent compilation on mainnet. @@ -227,6 +221,13 @@ mod tests { elf: CLAIMER_ELF.to_vec(), } } + + pub fn modified_transfer_program() -> Self { + use test_program_methods::MODIFIED_TRANSFER_ELF; + // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of + // `program_methods` + Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() + } } #[test] diff --git a/nssa/program_methods/guest/src/bin/modified_transfer.rs b/nssa/test_program_methods/guest/src/bin/modified_transfer.rs similarity index 100% rename from nssa/program_methods/guest/src/bin/modified_transfer.rs rename to nssa/test_program_methods/guest/src/bin/modified_transfer.rs From d7f03466710571ea572ab4500b84760e37d2353a Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 10 Dec 2025 15:57:43 -0300 Subject: [PATCH 62/70] fix test --- integration_tests/data_changer.bin | Bin 377792 -> 376260 bytes integration_tests/src/data_changer.bin | Bin 376260 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 integration_tests/src/data_changer.bin diff --git a/integration_tests/data_changer.bin b/integration_tests/data_changer.bin index 3d062c300fa1a9e6a5cc85e9888e5abaed93d751..eb28a627bac90378b6edd52dcf248751d866b6a1 100644 GIT binary patch delta 96469 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 BBMtxn delta 97335 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�m~#)?~G-VsdvRPsnlJtOj>mY%N$Yf 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%0DTs`9EZ8rBqREbf|(@fv*6vH;U3V8crac|gWcT# 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%0AhTUViH8wb7M} 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 From 1b2962fa03a9c9a28df1341bf79ec34272f2b17c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 10 Dec 2025 23:06:35 -0300 Subject: [PATCH 63/70] add missing check on pre_states for privacy tail calls --- nssa/core/src/account.rs | 4 ++-- .../guest/src/bin/privacy_preserving_circuit.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 89bec37..c152581 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -25,8 +25,8 @@ pub struct Account { pub nonce: Nonce, } -#[derive(Serialize, Deserialize, Clone)] -#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct AccountWithMetadata { pub account: Account, pub is_authorized: bool, 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 e8f184b..4cbc42c 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -45,14 +45,19 @@ fn main() { } // TODO: Modify when multi-chain calls are supported in the circuit - let Some(chained_call) = &caller.chained_calls.first() else { + let Some(caller_chained_call) = &caller.chained_calls.first() else { panic!("Expected chained call"); }; // Check that instruction data in caller is the instruction data in callee - if chained_call.instruction_data != callee.instruction_data { + if caller_chained_call.instruction_data != callee.instruction_data { panic!("Invalid instruction data"); } + + // Check that account pre_states in caller are the ones in calle + if caller_chained_call.pre_states != callee.pre_states { + panic!("Invalid pre states"); + } } for (i, program_output) in program_outputs.iter().enumerate() { From 2d8722b7d02e6b0d75af1d06512d56c93226a0da Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:59:28 -0500 Subject: [PATCH 64/70] nonce code shifting --- .../guest/src/bin/privacy_preserving_circuit.rs | 4 +--- nssa/src/privacy_preserving_transaction/circuit.rs | 2 +- nssa/src/state.rs | 6 ++++++ 3 files changed, 8 insertions(+), 4 deletions(-) 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 4cbc42c..29162db 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -141,9 +141,7 @@ fn main() { public_pre_states.push(pre_states[i].clone()); let mut post = state_diff.get(&pre_states[i].account_id).unwrap().clone(); - if pre_states[i].is_authorized { - post.nonce += 1; - } + if post.program_owner == DEFAULT_PROGRAM_ID { // Claim account post.program_owner = program_id; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index d6fc2c9..95933a3 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -195,7 +195,7 @@ mod tests { let expected_sender_post = Account { program_owner: program.id(), balance: 100 - balance_to_move, - nonce: 1, + nonce: 0, data: Data::default(), }; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index c3bbc3d..86df3a5 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -154,6 +154,12 @@ impl V02State { *current_account = post; } + // 5. Increment nonces for public signers + for account_id in tx.signer_account_ids() { + let current_account = self.get_account_by_id_mut(account_id); + current_account.nonce += 1; + } + Ok(()) } From 8c92a58bbc8293bbc3ab0a81d80c88f966bc9fc0 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 11 Dec 2025 20:59:37 -0300 Subject: [PATCH 65/70] add program deployment examples --- Cargo.toml | 2 + examples/program_deployment/Cargo.toml | 13 + examples/program_deployment/README.md | 568 ++++++++++++++++++ .../program_deployment/methods/Cargo.toml | 10 + examples/program_deployment/methods/build.rs | 3 + .../methods/guest/Cargo.toml | 13 + .../methods/guest/src/bin/hello_world.rs | 60 ++ .../src/bin/hello_world_with_authorization.rs | 69 +++ .../src/bin/hello_world_with_move_function.rs | 101 ++++ .../methods/guest/src/bin/simple_tail_call.rs | 60 ++ .../program_deployment/methods/src/lib.rs | 1 + .../src/bin/run_hello_world.rs | 67 +++ .../src/bin/run_hello_world_private.rs | 61 ++ .../bin/run_hello_world_through_tail_call.rs | 63 ++ .../bin/run_hello_world_with_authorization.rs | 80 +++ .../bin/run_hello_world_with_move_function.rs | 155 +++++ .../configs/debug/wallet/wallet_config.json | 3 +- nssa/src/lib.rs | 7 +- .../src/privacy_preserving_transaction/mod.rs | 2 + wallet/src/helperfunctions.rs | 2 +- wallet/src/lib.rs | 2 +- wallet/src/privacy_preserving_tx.rs | 22 +- 22 files changed, 1349 insertions(+), 15 deletions(-) create mode 100644 examples/program_deployment/Cargo.toml create mode 100644 examples/program_deployment/README.md create mode 100644 examples/program_deployment/methods/Cargo.toml create mode 100644 examples/program_deployment/methods/build.rs create mode 100644 examples/program_deployment/methods/guest/Cargo.toml create mode 100644 examples/program_deployment/methods/guest/src/bin/hello_world.rs create mode 100644 examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs create mode 100644 examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs create mode 100644 examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs create mode 100644 examples/program_deployment/methods/src/lib.rs create mode 100644 examples/program_deployment/src/bin/run_hello_world.rs create mode 100644 examples/program_deployment/src/bin/run_hello_world_private.rs create mode 100644 examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs create mode 100644 examples/program_deployment/src/bin/run_hello_world_with_authorization.rs create mode 100644 examples/program_deployment/src/bin/run_hello_world_with_move_function.rs diff --git a/Cargo.toml b/Cargo.toml index a54b91a..dd24d98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,9 @@ members = [ "sequencer_core", "common", "nssa", + "nssa/core", "integration_tests/proc_macro_test_attribute", + "examples/program_deployment", ] [workspace.dependencies] diff --git a/examples/program_deployment/Cargo.toml b/examples/program_deployment/Cargo.toml new file mode 100644 index 0000000..f704701 --- /dev/null +++ b/examples/program_deployment/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "program_deployment" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { workspace = true, features = ["macros"] } +wallet = { path = "../../wallet" } +nssa-core = {path = "../../nssa/core"} +nssa = { path = "../../nssa" } +key_protocol = { path = "../../key_protocol/" } +clap = "4.5.53" +serde = "1.0.228" diff --git a/examples/program_deployment/README.md b/examples/program_deployment/README.md new file mode 100644 index 0000000..c0d74ce --- /dev/null +++ b/examples/program_deployment/README.md @@ -0,0 +1,568 @@ +# Program deployment tutorial + +This guide walks you through running the sequencer, compiling example programs, deploying a Hello World program, and interacting with accounts. + +You'll find: +- Programs: example NSSA programs under `methods/guest/src/bin`. +- Runners: scripts to create and submit transactions to invoke these programs publicly and privately under `src/bin`. + +# 0. Install the wallet +From the project’s root directory: +```bash +cargo install --path wallet --force +``` + +# 1. Run the sequencer +From the project’s root directory, start the sequencer: +```bash +cd sequencer_runner +RUST_LOG=info cargo run $(pwd)/configs/debug +``` +Keep this terminal open. We’ll use it only to observe the node logs. + +> [!NOTE] +> If you have already ran this before you'll see a `rocksdb` directory with stored blocks. Be sure to remove that directory to follow this tutorial. + + +## Checking and setting up the wallet +For sanity let's check that the wallet can connect to it. + +```bash +wallet check-health +``` + +If this is your first time, the wallet will ask for a password. This is used as seed to deterministically generate all account keys (public and private). +For this tutorial, use: `program-tutorial` + +You should see `✅All looks good!` if everything went well. + +# 2. Compile the example programs +In a second terminal, from the `lssa` root directory, compile the example Risc0 programs: +```bash +cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +``` +The compiled `.bin` files will appear under: +``` +examples/program_deployment/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/ +``` +For convenience, export this path: +```bash +export EXAMPLE_PROGRAMS_BUILD_DIR=$(pwd)/examples/program_deployment/methods/guest/target/riscv32im-risc0-zkvm-elf/docker +``` + +# 3. Hello world example + +The Hello world program reads an arbitrary sequence of bytes from its instruction and appends them to the data field of the input account. +Execution succeeds only if the account is: + +- Uninitialized, or +- Already owned by this program + +If uninitialized, the program will claim the account and emit the updated state. + +## Navigate to the example directory +All remaining commands must be run from: +```bash +cd examples/program_deployment +``` + +## Deploy the Program + +Use the wallet’s built-in program deployment command: +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin +``` + +# 4. Public execution of the Hello world example + +## Create a Public Account + +Generate a new public account: +```bash +wallet account new public +``` + +You'll see an output similar to: +```bash +Generated new account with account_id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 at path /0 +``` +The relevant part is the account id `BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9` + +## Check the account state +New accounts are always Uninitialized. Verify: +```bash +wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` +Expected output: +``` +Account is Uninitialized +``` +The `Public/` prefix tells the wallet to query the public state. + +## Execute the Hello world program +Run the example: +```bash +cargo run --bin run_hello_world \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin \ + BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` +> [!NOTE] +> - Passing the `.bin` lets the script compute the program ID and build the transaction. +> - Because this program executes publicly, the node performs the execution. +> - The program will claim the account and write data into it. + +Monitor the sequencer terminal to confirm execution. + +## Inspect the updated account +After the transaction is processed, check the new state: +```bash +wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` +Example output: +```json +{ + "balance": 0, + "program_owner_b64": "o6C6/bbjDmN9VUC51McBpPrta8lxrx2X0iHExhX0yNU=", + "data_b64": "SG9sYSBtdW5kbyE=", + "nonce": 0 +} +``` +The `data_b64` field contains de data in Base64. +Decode it: +```bash +echo -n SG9sYSBtdW5kbyE= | base64 -d +``` +You should see `Hola mundo!`. + +# 5. Understanding the code in `hello_world.rs`. +The Hello world example demonstrates the minimal structure of an NSSA program. +Its purpose is very simple: append the instruction bytes to the data field of a single account. + +### What this program does in a nutshell +1. Reads the program inputs + - The list of pre-state accounts (`pre_states`) + - The instruction bytes (`instruction`) + - The raw instruction data (used again when writing outputs) +2. Checks that there is exactly one input account: this example operates on a single account, so it expects `pre_states` to contain exactly one entry. +3. Builds the post-state: It clones the input account and appends the instruction bytes to its data field. +4. Handles account claiming logic: If the account is uninitialized (i.e. not yet claimed by any program), its program_owner will equal `DEFAULT_PROGRAM_ID`. In that case, the program issues a claim request, meaning: "This program now owns this account." +5. Outputs the proposed state transition: `write_nssa_outputs` emits: + - The original instruction data + - The original pre-states + - The new post-states + +## Code walkthrough +1. Reading inputs: +```rust +let (ProgramInput { pre_states, instruction: greeting }, instruction_data) + = read_nssa_inputs::(); +``` +2. Extracting the single account: +```rust +let [pre_state] = pre_states + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); +``` +3. Constructing the updated account post state +```rust +let mut this = pre_state.account.clone(); +let mut bytes = this.data.into_inner(); +bytes.extend_from_slice(&greeting); +this.data = bytes.try_into().expect("Data should fit within the allowed limits"); +``` +4. Instantiating the `AccountPostState` with a claiming request only if the account pre state is uninitialized: +```rust +let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { + AccountPostState::new_claimed(post_account) +} else { + AccountPostState::new(post_account) +}; +``` +5. Emmiting the output +```rust +write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); +``` + +# 6. Understanding the runner script `run_hello_world.rs` +The `run_hello_world.rs` example demonstrates how to construct and submit a public transaction that executes the `hello_world` program. Below is a breakdown of what the file does and how the pieces fit together. + +### 1. Wallet initialization +```rust +let wallet_config = fetch_config().await.unwrap(); +let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); +``` +The example loads the wallet configuration and initializes `WalletCore`. +This gives access to: +- the sequencer client, +- the wallet’s account storage. + +### 2. Parsing inputs +```rust +let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); +let account_id: AccountId = std::env::args_os().nth(2).unwrap().into_string().unwrap().parse().unwrap(); +``` +The program expects two arguments: +- Path to the guest binary +- AccountId of the public account to operate on + +This is the account that the program will claim and write data into. + +### 3. Loading the program bytecode +```rust +let bytecode: Vec = std::fs::read(program_path).unwrap(); +let program = Program::new(bytecode).unwrap(); +``` +The Risc0 ELF is read from disk and wrapped in a Program object, which can be used to compute the program ID. The ID is used by the node to identify which program is invoked by the transaction. + + +### 4. Preparing the instruction data +```rust +let greeting: Vec = vec![72,111,108,97,32,109,117,110,100,111,33]; +``` +The example hardcodes the ASCII bytes for `Hola mundo!`. These bytes are passed to the program as its “instruction,” which the Hello World program simply appends to the account’s data field. + +### 5. Creating the public transaction + +```rust +let nonces = vec![]; +let signing_keys = []; +let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap(); +let witness_set = WitnessSet::for_message(&message, &signing_keys); +let tx = PublicTransaction::new(message, witness_set); +``` + +A public transaction consists of: +- a `Message` +- a corresponding `WitnessSet` + +For this simple example, no signing or nonces are required. The transaction includes only the program ID, the target account, and the instruction bytes. The Hello World program allows this because it does not explicitly require authorization. In the next example, we’ll see how authorization requirements are enforced and how to construct a transaction that includes signatures and nonces. + +### 6. Submitting the transaction +```rust +let response = wallet_core.sequencer_client.send_tx_public(tx).await.unwrap(); +``` +The transaction is sent to the sequencer, which processes it and updates the public state accordingly. + +Once executed, you’ll be able to query the updated account to see the newly written "Hola mundo!" data. + +# 7. Private execution of the Hello world example + +This section is very similar to the previous case: + +## Create a private account + +Generate a new private account: +```bash +wallet account new private +``` + +You'll see an output similar to: +```bash +Generated new account with account_id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr at path /0 +``` +The relevant part for this tutorial is the account id `7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr` + +You can check it's uninitialized with + +```bash +wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr +``` + +## Privately executing the Hello world program + +### Execute the Hello world program +Run the example: +```bash +cargo run --bin run_hello_world_private \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin \ + 7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr +``` +> [!NOTE] +> - This command may take a few minutes to complete. A ZK proof of the Hello world program execution and the privacy preserving circuit are being generated. Depending on the machine this can take from 30 seconds to 4 minutes. +> - We are passing the same `hello_world.bin` binary as in the previous case with public executions. This is because the program is the same, it is the privacy context of the input account that's different. +> - Because this program executes privately, the local machine runs the program and generate the proof of execution. +> - The program will claim the private account and write data into it. + +### Syncing the new private account values +The `run_hello_world` script submitted a transaction and it was (hopefully) accepted by the node. On chain there is now a commitment to the new private account values, and the account data is stored encrypted. However, the local client hasn’t updated its private state yet. That’s why, if you try to get the private account values now, it still reads the old values from local storage instead. + +```bash +wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr +``` + +This will still show `Account is Uninitialized`. To see the new values locally, you need to run the wallet sync command. Once the client syncs, the local store will reflect the updated account data. + +To sync private accounts run: +```bash +wallet account sync-private +``` +> [!NOTE] +> - This queries the node for transactions and goes throught the encrypted accounts. Whenever a new value is found for one of the owned private accounts, the local storage is updated. + +After this completes, running +```bash +wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr +``` +should show something similar to +```json +{ + "balance":0, + "program_owner_b64":"dWgtNRixwjC0C8aA0NL0Iuss3Q26Dw6ECk7bzExW4bI=", + "data_b64":"SG9sYSBtdW5kbyE=", + "nonce":236788677072686551559312843688143377080 +} +``` + +## The `run_hello_world_private.rs` runner +This example extends the public `run_hello_world.rs` flow by constructing a privacy-preserving transaction instead of a public one. +Both runners load a guest program, prepare a transaction, and submit it. But the private version handles encrypted account data, nullifiers, ephemeral keys, and zk proofs. + +Unlike the public version, `run_hello_world_private.rs` must: +- prepare the private account pre-state (nullifier keys, membership proof, encrypted values) +- derive a shared secret to encrypt the post-state +- compute the correct visibility mask (initialized vs. uninitialized private account) +- execute the guest program inside the zkVM and produce a proof +- build a PrivacyPreservingTransaction composed of: +- a Message encoding commitments + encrypted post-state +- a WitnessSet embedding the zk proof + +Luckily all that complexity is hidden behind the `wallet_core.send_privacy_preserving_tx` function: +```rust + let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + + // Construct and submit the privacy-preserving transaction + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(greeting).unwrap(), + &program, + ) + .await + .unwrap(); +``` +Check the `run_hello_world_private.rs` file to see how it is used. + +# 8. Account authorization mechanism +The Hello world example does not enforce any authorization on the input account. This means any user can execute it on any account, regardless of ownership. +NSSA provides a mechanism for programs to enforce proper authorization before an execution can succeed. The meaning of authorization differs between public and private accounts: +- Public accounts: authorization requires that the transaction is signed with the account’s signing key. +- Private accounts: authorization requires that the circuit verifies knowledge of the account’s nullifier secret key. + +From the program development perspective it is very simple: input accounts come with a flag indicating whether they has been properly authorized. And so, the only difference between the program `hello_world.rs` and `hello_world_with_authorization.rs` is in the lines + +```rust + // #### Difference with `hello_world` example here: + // Fail if the input account is not authorized + // The `is_authorized` field will be correctly populated or verified by the system if + // authorization is provided. + if !pre_state.is_authorized { + panic!("Missing required authorization"); + } + // #### +``` + +Which just checks the `is_authorized` flag and fails if it is set to false. + +# 9. Public execution of the Hello world with authorization example +The workflow to execute it publicly is very similar: + +### Deploy the program +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin +``` + +### Create a new public account +Our previous public account is already claimed by the simple Hello world program. So we need a new one to work with this other version of the hello program +```bash +wallet account new public +``` + +Outupt: +``` +Generated new account with account_id Public/9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE at path /1 +``` + +### Run the program + +```bash +cargo run --bin run_hello_world_with_authorization \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin \ + 9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE +``` + +# 10. Understanding `run_hello_world_with_authorization.rs` +From the runner script perspective, the only difference is that the signing keys are passed to the `WitnessSet` constructor for it to sign it. You can see this in the following parts of the code: + +1. Loading the sigining keys from the wallet storage +```rust + // Load signing keys to provide authorization + let signing_key = wallet_core + .storage + .user_data + .get_pub_account_signing_key(&account_id) + .expect("Input account should be a self owned public account"); +``` +2. Fetching the current public nonce. +```rust + // Construct the public transaction + // Query the current nonce from the node + let nonces = wallet_core + .get_accounts_nonces(vec![account_id]) + .await + .expect("Node should be reachable to query account data"); +``` +2. Instantiate the witness set using the signing keys +```rust + let signing_keys = [signing_key]; + let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap(); + // Pass the signing key to sign the message. This will be used by the node + // to flag the pre_state as `is_authorized` when executing the program + let witness_set = WitnessSet::for_message(&message, &signing_keys); +``` + +## Seeing the mechanism in action +If everything went well you won't notice any difference with the first Hello world, because the runner takes care of signing the transaction to provide authorization and the program just succeeds. +Try using the `run_hello_world.rs` runner with the `hello_world_with_authorization.bin` program. This will fail because the runner will submit the transaction without the corresponding signature. +```bash +cargo run --bin run_hello_world \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin \ + 9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE +``` + +You should see something like the following **on the node logs**. +```bash +[2025-12-11T13:43:22Z WARN sequencer_core] Error at transition ProgramExecutionFailed( + "Guest panicked: Missing required authorization", + ) +``` + +# 11. Public and private account interaction example +Previous examples only operated on public or private accounts independently. Those minimal programs were useful to introduce basic concepts, but they couldn't demonstrate how different types of accounts interact within a single program invocation. +The "Hello world with move function" introduces two operations that require one or two input accounts: +- `write`: appends arbitrary bytes to a single account. This is what we already had. +- `move_data`: reads all bytes from one account, clears it, and appends those bytes to another account. +Because these operations may involve multiple accounts, we'll see how public and private accounts can participate together in one execution. It highlights how ownership checks work, when an account needs to be claimed, and how multiple post-states are emitted when several accounts are modified. + +> [!NOTE] +> The program logic is completely agnostic to whether input accounts are public or private. It always executes the same way. +> See `methods/guest/src/bin/hello_world_with_move_function.rs`. The program just reads the instruction bytes and updates the accounts state. +> All privacy handling happens on the runner side. When constructing the transaction, the runner decides which accounts are public or private and prepares the appropriate proofs. The program itself can't differentiate between privacy modes. + +Let's start by deploying the program +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin +``` + +Let's also create a new public account +```bash +wallet account new public +``` + +Output: +``` +Generated new account with account_id Public/95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs at path /0/0 +``` + +Let's execute the write function + +```bash +cargo run --bin run_hello_world_with_move_function \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \ + write-public 95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs mundo! +``` + +Let's crate a new private account. + +```bash +wallet account new private +``` + +Output: +``` +Generated new account with account_id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU at path /1 +``` + +Let's execute the write function + +```bash +cargo run --bin run_hello_world_with_move_function \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \ + write-private 8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU Hola +``` + +To check the values of the accounts are as expected run: +```bash +wallet account get --account-id Public/95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs +``` +and + +```bash +wallet account sync-private +wallet account get --account-id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU +``` + +and check the (base64 encoded) data values are `mundo!` and `Hola` respectively. + +Now we can execute the move function to clear the data on the public account and move it to the private account. + +```bash +cargo run --bin run_hello_world_with_move_function \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \ + move-data-public-to-private 95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs 8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU +``` + +After succeeding, re run the get and sync commands and check that the public account has empty data and the private account data is `Holamundo!`. + +# 12. Program composition: tail calls +Programs can chain calls to other programs when they return. This is the tail call or chained call mechanism. It is used by programs that depend on other programs. + +The examples include a `guest/src/bin/simple_tail_call.rs` program that shows how to trigger this mechanism. It internally calls the first Hello World program with a fixed greeting: `Hello from tail call`. + +> [!NOTE] +> This program hardcodes the ID of the Hello World program. If something fails, check that this ID matches the one produced when building the Hello World program. You can see it in the output of `cargo risczero build` from the earlier sections of this tutorial. If it differs, update the ID in `simple_tail_call.rs` and build again. + +As before, let's start by deploying the program + +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/simple_tail_call.bin +``` + +We'll use the first public account of this tutorial. The one with account id `BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9`. This account is already owned by the Hello world program and its data reads `Hola mundo!`. + +Let's run the tail call program + +```bash +cargo run --bin run_hello_world_through_tail_call \ + $EXAMPLE_PROGRAMS_BUILD_DIR/simple_tail_call.bin \ + BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` + +Once the transaction is processed, query the account values with: + +```bash +wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` + +You should se an output similar to + +```json +{ + "balance":0, + "program_owner_b64":"fpnW4tFY9N6llZcBHaXRwu7xe+7WZnZX9RWzhwNbk1o=", + "data_b64":"SG9sYSBtdW5kbyFIZWxsbyBmcm9tIHRhaWwgY2FsbA==", + "nonce":0 +} +``` + +Decoding the (base64 encoded) data +```bash +echo -n SG9sYSBtdW5kbyFIZWxsbyBmcm9tIHRhaWwgY2FsbA== | base64 -d +``` + +Output: +``` +Hola mundo!Hello from tail call +``` + diff --git a/examples/program_deployment/methods/Cargo.toml b/examples/program_deployment/methods/Cargo.toml new file mode 100644 index 0000000..0317d2b --- /dev/null +++ b/examples/program_deployment/methods/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test-program-methods" +version = "0.1.0" +edition = "2024" + +[build-dependencies] +risc0-build = { version = "3.0.3" } + +[package.metadata.risc0] +methods = ["guest"] diff --git a/examples/program_deployment/methods/build.rs b/examples/program_deployment/methods/build.rs new file mode 100644 index 0000000..08a8a4e --- /dev/null +++ b/examples/program_deployment/methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/examples/program_deployment/methods/guest/Cargo.toml b/examples/program_deployment/methods/guest/Cargo.toml new file mode 100644 index 0000000..8e2a199 --- /dev/null +++ b/examples/program_deployment/methods/guest/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "programs" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "3.0.3", features = ['std'] } +nssa-core = { path = "../../../../nssa/core" } +serde = { version = "1.0.219", default-features = false } +hex = "0.4.3" +bytemuck = "1.24.0" diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world.rs b/examples/program_deployment/methods/guest/src/bin/hello_world.rs new file mode 100644 index 0000000..3391eb5 --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/hello_world.rs @@ -0,0 +1,60 @@ +use nssa_core::program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, +}; + +// Hello-world example program. +// +// This program reads an arbitrary sequence of bytes as its instruction +// and appends those bytes to the `data` field of the single input account. +// +// Execution succeeds only if the input account is either: +// - uninitialized, or +// - already owned by this program. +// +// In case the input account is uninitialized, the program claims it. +// +// The updated account is emitted as the sole post-state. + +type Instruction = Vec; + +fn main() { + // Read inputs + let ( + ProgramInput { + pre_states, + instruction: greeting, + }, + instruction_data, + ) = read_nssa_inputs::(); + + // Unpack the input account pre state + let [pre_state] = pre_states + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); + + // Construct the post state account values + let post_account = { + let mut this = pre_state.account.clone(); + let mut bytes = this.data.into_inner(); + bytes.extend_from_slice(&greeting); + this.data = bytes + .try_into() + .expect("Data should fit within the allowed limits"); + this + }; + + // Wrap the post state account values inside a `AccountPostState` instance. + // This is used to forward the account claiming request if any + let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { + // This produces a claim request + AccountPostState::new_claimed(post_account) + } else { + // This doesn't produce a claim request + AccountPostState::new(post_account) + }; + + // The output is a proposed state difference. It will only succeed if the pre states coincide + // with the previous values of the accounts, and the transition to the post states conforms + // with the NSSA program rules. + write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); +} diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs new file mode 100644 index 0000000..043da1b --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs @@ -0,0 +1,69 @@ +use nssa_core::program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, +}; + +// Hello-world with authorization example program. +// +// This program reads an arbitrary sequence of bytes as its instruction +// and appends those bytes to the `data` field of the single input account. +// +// Execution succeeds only if the input account **is authorized** and is either: +// - uninitialized, or +// - already owned by this program. +// +// In case the input account is uninitialized, the program claims it. +// +// The updated account is emitted as the sole post-state. + +type Instruction = Vec; + +fn main() { + // Read inputs + let ( + ProgramInput { + pre_states, + instruction: greeting, + }, + instruction_data, + ) = read_nssa_inputs::(); + + // Unpack the input account pre state + let [pre_state] = pre_states + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); + + // #### Difference with `hello_world` example here: + // Fail if the input account is not authorized + // The `is_authorized` field will be correctly populated or verified by the system if + // authorization is provided. + if !pre_state.is_authorized { + panic!("Missing required authorization"); + } + // #### + + // Construct the post state account values + let post_account = { + let mut this = pre_state.account.clone(); + let mut bytes = this.data.into_inner(); + bytes.extend_from_slice(&greeting); + this.data = bytes + .try_into() + .expect("Data should fit within the allowed limits"); + this + }; + + // Wrap the post state account values inside a `AccountPostState` instance. + // This is used to forward the account claiming request if any + let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { + // This produces a claim request + AccountPostState::new_claimed(post_account) + } else { + // This doesn't produce a claim request + AccountPostState::new(post_account) + }; + + // The output is a proposed state difference. It will only succeed if the pre states coincide + // with the previous values of the accounts, and the transition to the post states conforms + // with the NSSA program rules. + write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); +} diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs new file mode 100644 index 0000000..af0d4bf --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs @@ -0,0 +1,101 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, + }, +}; + +// Hello-world with write + move_data example program. +// +// This program reads an instruction of the form `(function_id, data)` and +// dispatches to either: +// +// - `write`: appends `data` to the `data` field of a single input account. +// - `move_data`: moves all bytes from one account to another. The source account is cleared and the +// destination account receives the appended bytes. +// +// Execution succeeds only if: +// - the accounts involved are either uninitialized, or +// - already owned by this program. +// +// In case an input account is uninitialized, the program will claim it when +// producing the post-state. + +type Instruction = (u8, Vec); +const WRITE_FUNCTION_ID: u8 = 0; +const MOVE_DATA_FUNCTION_ID: u8 = 1; + +fn build_post_state(post_account: Account) -> AccountPostState { + if post_account.program_owner == DEFAULT_PROGRAM_ID { + // This produces a claim request + AccountPostState::new_claimed(post_account) + } else { + // This doesn't produce a claim request + AccountPostState::new(post_account) + } +} + +fn write(pre_state: AccountWithMetadata, greeting: Vec) -> AccountPostState { + // Construct the post state account values + let post_account = { + let mut this = pre_state.account.clone(); + let mut bytes = this.data.into_inner(); + bytes.extend_from_slice(&greeting); + this.data = bytes + .try_into() + .expect("Data should fit within the allowed limits"); + this + }; + + build_post_state(post_account) +} + +fn move_data( + from_pre: &AccountWithMetadata, + to_pre: &AccountWithMetadata, +) -> Vec { + // Construct the post state account values + let from_data: Vec = from_pre.account.data.clone().into(); + + let from_post = { + let mut this = from_pre.account.clone(); + this.data = Default::default(); + build_post_state(this) + }; + + let to_post = { + let mut this = to_pre.account.clone(); + let mut bytes = this.data.into_inner(); + bytes.extend_from_slice(&from_data); + this.data = bytes + .try_into() + .expect("Data should fit within the allowed limits"); + build_post_state(this) + }; + + vec![from_post, to_post] +} + +fn main() { + // Read input accounts. + let ( + ProgramInput { + pre_states, + instruction: (function_id, data), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let post_states = match (pre_states.as_slice(), function_id, data.len()) { + ([account_pre], WRITE_FUNCTION_ID, _) => { + let post = write(account_pre.clone(), data); + vec![post] + } + ([account_from_pre, account_to_pre], MOVE_DATA_FUNCTION_ID, 0) => { + move_data(account_from_pre, account_to_pre) + } + _ => panic!("invalid params"), + }; + + write_nssa_outputs(instruction_words, pre_states, post_states); +} diff --git a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs new file mode 100644 index 0000000..5d8e221 --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs @@ -0,0 +1,60 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, +}; + +// Tail Call example program. +// +// This program shows how to chain execution to another program using `ChainedCall`. +// It reads a single account, emits it unchanged, and then triggers a tail call +// to the Hello World program with a fixed greeting. + + +const HELLO_WORLD_PROGRAM_ID_HEX: &str = + "7e99d6e2d158f4dea59597011da5d1c2eef17beed6667657f515b387035b935a"; + +fn hello_world_program_id() -> ProgramId { + let hello_world_program_id_bytes: [u8; 32] = hex::decode(HELLO_WORLD_PROGRAM_ID_HEX) + .unwrap() + .try_into() + .unwrap(); + bytemuck::cast(hello_world_program_id_bytes) +} + +fn main() { + // Read inputs + let ( + ProgramInput { + pre_states, + instruction: _, + }, + instruction_data, + ) = read_nssa_inputs::<()>(); + + // Unpack the input account pre state + let [pre_state] = pre_states + .clone() + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); + + // Create the (unchanged) post state + let post_state = AccountPostState::new(pre_state.account.clone()); + + // Create the chained call + let chained_call_greeting: Vec = b"Hello from tail call".to_vec(); + let chained_call_instruction_data = risc0_zkvm::serde::to_vec(&chained_call_greeting).unwrap(); + let chained_call = ChainedCall { + program_id: hello_world_program_id(), + instruction_data: chained_call_instruction_data, + pre_states, + pda_seeds: vec![], + }; + + // Write the outputs + write_nssa_outputs_with_chained_call( + instruction_data, + vec![pre_state], + vec![post_state], + vec![chained_call], + ); +} diff --git a/examples/program_deployment/methods/src/lib.rs b/examples/program_deployment/methods/src/lib.rs new file mode 100644 index 0000000..1bdb308 --- /dev/null +++ b/examples/program_deployment/methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/examples/program_deployment/src/bin/run_hello_world.rs b/examples/program_deployment/src/bin/run_hello_world.rs new file mode 100644 index 0000000..a7dc0fc --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world.rs @@ -0,0 +1,67 @@ +use nssa::{ + AccountId, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet}, +}; +use wallet::{WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `hello_world.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin +// +// +// Usage: +// cargo run --bin run_hello_world /path/to/guest/binary +// +// Example: +// cargo run --bin run_hello_world \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the program binary + let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(2) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Define the desired greeting in ASCII + let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; + + // Construct the public transaction + // No nonces nor signing keys are needed for this example. Check out the + // `run_hello_world_with_authorization` on how to use them. + let nonces = vec![]; + let signing_keys = []; + let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap(); + let witness_set = WitnessSet::for_message(&message, &signing_keys); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_private.rs b/examples/program_deployment/src/bin/run_hello_world_private.rs new file mode 100644 index 0000000..be4280b --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_private.rs @@ -0,0 +1,61 @@ +use nssa::{AccountId, program::Program}; +use wallet::{PrivacyPreservingAccount, WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `hello_world.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin +// +// +// Usage: +// cargo run --bin run_hello_world_private /path/to/guest/binary +// +// Note: the provided account_id needs to be of a private self owned account +// +// Example: +// cargo run --bin run_hello_world_private \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the program binary + let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(2) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Define the desired greeting in ASCII + let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; + + let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + + // Construct and submit the privacy-preserving transaction + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(greeting).unwrap(), + &program, + ) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs b/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs new file mode 100644 index 0000000..d7c91f8 --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs @@ -0,0 +1,63 @@ +use nssa::{ + AccountId, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet}, +}; +use wallet::{WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `simple_tail_call.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin +// +// +// Usage: +// cargo run --bin run_hello_world_through_tail_call /path/to/guest/binary +// +// Example: +// cargo run --bin run_hello_world_through_tail_call \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the program binary + let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(2) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + let instruction_data = (); + let nonces = vec![]; + let signing_keys = []; + let message = + Message::try_new(program.id(), vec![account_id], nonces, instruction_data).unwrap(); + let witness_set = WitnessSet::for_message(&message, &signing_keys); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs b/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs new file mode 100644 index 0000000..21740ae --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs @@ -0,0 +1,80 @@ +use nssa::{ + AccountId, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet}, +}; +use wallet::{WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `hello_world_with_authorization.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_authorization.bin +// +// +// Usage: +// ./run_hello_world_with_authorization /path/to/guest/binary +// +// Note: the provided account_id needs to be of a public self owned account +// +// Example: +// cargo run --bin run_hello_world_with_authorization \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_authorization.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the program binary + let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(2) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Load signing keys to provide authorization + let signing_key = wallet_core + .storage + .user_data + .get_pub_account_signing_key(&account_id) + .expect("Input account should be a self owned public account"); + + // Define the desired greeting in ASCII + let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; + + // Construct the public transaction + // Query the current nonce from the node + let nonces = wallet_core + .get_accounts_nonces(vec![account_id]) + .await + .expect("Node should be reachable to query account data"); + let signing_keys = [signing_key]; + let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap(); + // Pass the signing key to sign the message. This will be used by the node + // to flag the pre_state as `is_authorized` when executing the program + let witness_set = WitnessSet::for_message(&message, &signing_keys); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs new file mode 100644 index 0000000..77c2597 --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs @@ -0,0 +1,155 @@ +use clap::{Parser, Subcommand}; +use nssa::{PublicTransaction, program::Program, public_transaction}; +use wallet::{PrivacyPreservingAccount, WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `hello_world_with_move_function.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_move_function.bin +// +// +// Usage: +// cargo run --bin run_hello_world_with_move_function /path/to/guest/binary +// +// Example: +// cargo run --bin run_hello_world_with_move_function \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_move_function.bin \ +// write-public Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE Hola + +type Instruction = (u8, Vec); +const WRITE_FUNCTION_ID: u8 = 0; +const MOVE_DATA_FUNCTION_ID: u8 = 1; + +#[derive(Parser, Debug)] +struct Cli { + /// Path to program binary + program_path: String, + + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Write instruction into one account + WritePublic { + account_id: String, + greeting: String, + }, + WritePrivate { + account_id: String, + greeting: String, + }, + /// Move data between two accounts + MoveDataPublicToPublic { + from: String, + to: String, + }, + MoveDataPublicToPrivate { + from: String, + to: String, + }, +} + +#[tokio::main] +async fn main() { + let cli = Cli::parse(); + + // Load the program + let bytecode: Vec = std::fs::read(cli.program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + match cli.command { + Command::WritePublic { + account_id, + greeting, + } => { + let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes()); + let account_id = account_id.parse().unwrap(); + let nonces = vec![]; + let message = public_transaction::Message::try_new( + program.id(), + vec![account_id], + nonces, + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); + } + Command::WritePrivate { + account_id, + greeting, + } => { + let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes()); + let account_id = account_id.parse().unwrap(); + let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(instruction).unwrap(), + &program, + ) + .await + .unwrap(); + } + Command::MoveDataPublicToPublic { from, to } => { + let instruction: Instruction = (MOVE_DATA_FUNCTION_ID, vec![]); + let from = from.parse().unwrap(); + let to = to.parse().unwrap(); + let nonces = vec![]; + let message = public_transaction::Message::try_new( + program.id(), + vec![from, to], + nonces, + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); + } + Command::MoveDataPublicToPrivate { from, to } => { + let instruction: Instruction = (MOVE_DATA_FUNCTION_ID, vec![]); + let from = from.parse().unwrap(); + let to = to.parse().unwrap(); + + let accounts = vec![ + PrivacyPreservingAccount::Public(from), + PrivacyPreservingAccount::PrivateOwned(to), + ]; + + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(instruction).unwrap(), + &program, + ) + .await + .unwrap(); + } + }; +} diff --git a/integration_tests/configs/debug/wallet/wallet_config.json b/integration_tests/configs/debug/wallet/wallet_config.json index ac4bae8..ad7b279 100644 --- a/integration_tests/configs/debug/wallet/wallet_config.json +++ b/integration_tests/configs/debug/wallet/wallet_config.json @@ -542,5 +542,6 @@ } } } - ] + ], + "basic_auth": null } \ No newline at end of file diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index b698ae3..e7182c9 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -17,7 +17,12 @@ pub mod public_transaction; mod signature; mod state; -pub use nssa_core::account::{Account, AccountId}; +pub use nssa_core::{ + SharedSecretKey, + account::{Account, AccountId}, + encryption::EphemeralPublicKey, + program::ProgramId, +}; pub use privacy_preserving_transaction::{ PrivacyPreservingTransaction, circuit::execute_and_prove, }; diff --git a/nssa/src/privacy_preserving_transaction/mod.rs b/nssa/src/privacy_preserving_transaction/mod.rs index c74c077..48d8818 100644 --- a/nssa/src/privacy_preserving_transaction/mod.rs +++ b/nssa/src/privacy_preserving_transaction/mod.rs @@ -4,4 +4,6 @@ pub mod witness_set; pub mod circuit; +pub use message::Message; pub use transaction::PrivacyPreservingTransaction; +pub use witness_set::WitnessSet; diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 5f1dcf7..eaa00a5 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -190,7 +190,7 @@ pub fn produce_data_for_storage( } } -pub(crate) fn produce_random_nonces(size: usize) -> Vec { +pub fn produce_random_nonces(size: usize) -> Vec { let mut result = vec![[0; 16]; size]; result.iter_mut().for_each(|bytes| OsRng.fill_bytes(bytes)); result.into_iter().map(Nonce::from_le_bytes).collect() diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index bc28311..d679cd1 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -35,7 +35,7 @@ pub mod cli; pub mod config; pub mod helperfunctions; pub mod poller; -mod privacy_preserving_tx; +pub mod privacy_preserving_tx; pub mod program_facades; pub struct WalletCore { diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index e8e14d9..5dec8af 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -61,7 +61,7 @@ impl AccountManager { } PrivacyPreservingAccount::PrivateOwned(account_id) => { let pre = private_acc_preparation(wallet, account_id).await?; - let mask = if pre.auth_acc.is_authorized { 1 } else { 2 }; + let mask = if pre.pre_state.is_authorized { 1 } else { 2 }; (State::Private(pre), mask) } @@ -72,7 +72,7 @@ impl AccountManager { nsk: None, npk, ipk, - auth_acc, + pre_state: auth_acc, proof: None, }; @@ -95,7 +95,7 @@ impl AccountManager { .iter() .map(|state| match state { State::Public { account, .. } => account.clone(), - State::Private(pre) => pre.auth_acc.clone(), + State::Private(pre) => pre.pre_state.clone(), }) .collect() } @@ -164,15 +164,15 @@ impl AccountManager { } } -struct AccountPreparedData { - nsk: Option, - npk: NullifierPublicKey, - ipk: IncomingViewingPublicKey, - auth_acc: AccountWithMetadata, - proof: Option, +pub struct AccountPreparedData { + pub nsk: Option, + pub npk: NullifierPublicKey, + pub ipk: IncomingViewingPublicKey, + pub pre_state: AccountWithMetadata, + pub proof: Option, } -async fn private_acc_preparation( +pub async fn private_acc_preparation( wallet: &WalletCore, account_id: AccountId, ) -> Result { @@ -206,7 +206,7 @@ async fn private_acc_preparation( nsk, npk: from_npk, ipk: from_ipk, - auth_acc: sender_pre, + pre_state: sender_pre, proof, }) } From bb58f7dd0a69fb36bc6669a82a534d610962888f Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 11 Dec 2025 21:15:57 -0300 Subject: [PATCH 66/70] revert changes --- wallet/src/helperfunctions.rs | 2 +- wallet/src/lib.rs | 2 +- wallet/src/privacy_preserving_tx.rs | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index eaa00a5..5f1dcf7 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -190,7 +190,7 @@ pub fn produce_data_for_storage( } } -pub fn produce_random_nonces(size: usize) -> Vec { +pub(crate) fn produce_random_nonces(size: usize) -> Vec { let mut result = vec![[0; 16]; size]; result.iter_mut().for_each(|bytes| OsRng.fill_bytes(bytes)); result.into_iter().map(Nonce::from_le_bytes).collect() diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index d679cd1..bc28311 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -35,7 +35,7 @@ pub mod cli; pub mod config; pub mod helperfunctions; pub mod poller; -pub mod privacy_preserving_tx; +mod privacy_preserving_tx; pub mod program_facades; pub struct WalletCore { diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 5dec8af..e79bbac 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -164,15 +164,15 @@ impl AccountManager { } } -pub struct AccountPreparedData { - pub nsk: Option, - pub npk: NullifierPublicKey, - pub ipk: IncomingViewingPublicKey, - pub pre_state: AccountWithMetadata, - pub proof: Option, +struct AccountPreparedData { + nsk: Option, + npk: NullifierPublicKey, + ipk: IncomingViewingPublicKey, + pre_state: AccountWithMetadata, + proof: Option, } -pub async fn private_acc_preparation( +async fn private_acc_preparation( wallet: &WalletCore, account_id: AccountId, ) -> Result { From 3e3ee9ba3639221d0cfb59d2ea5453cf59f5532d Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Dec 2025 10:30:57 -0300 Subject: [PATCH 67/70] taplo fmt --- examples/program_deployment/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/program_deployment/Cargo.toml b/examples/program_deployment/Cargo.toml index f704701..21d4fc8 100644 --- a/examples/program_deployment/Cargo.toml +++ b/examples/program_deployment/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] tokio = { workspace = true, features = ["macros"] } wallet = { path = "../../wallet" } -nssa-core = {path = "../../nssa/core"} +nssa-core = { path = "../../nssa/core" } nssa = { path = "../../nssa" } key_protocol = { path = "../../key_protocol/" } clap = "4.5.53" From 7453734dbeb6faf270388c3fddf7e5b6e6adb0e8 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Dec 2025 10:54:33 -0300 Subject: [PATCH 68/70] add important note --- examples/program_deployment/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/program_deployment/README.md b/examples/program_deployment/README.md index c0d74ce..1bc7ed7 100644 --- a/examples/program_deployment/README.md +++ b/examples/program_deployment/README.md @@ -50,6 +50,9 @@ For convenience, export this path: export EXAMPLE_PROGRAMS_BUILD_DIR=$(pwd)/examples/program_deployment/methods/guest/target/riscv32im-risc0-zkvm-elf/docker ``` +> [!IMPORTANT] +> **All remaining commands must be run from the `examples/program_deployment` directory.** + # 3. Hello world example The Hello world program reads an arbitrary sequence of bytes from its instruction and appends them to the data field of the input account. From 63f102643becd1375c1a5409e9284201eb9f932f Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Dec 2025 14:30:36 -0300 Subject: [PATCH 69/70] add instructions to get the hello world program id in hex --- .../methods/guest/src/bin/simple_tail_call.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs index 5d8e221..d2bb58c 100644 --- a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs +++ b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs @@ -10,6 +10,10 @@ use nssa_core::program::{ // to the Hello World program with a fixed greeting. +/// This needs to be set to the ID of the Hello world program. +/// To get the ID run **from the root directoy of the repository**: +/// `cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml` +/// This compiles the programs and outputs the IDs in hex that can be used to copy here. const HELLO_WORLD_PROGRAM_ID_HEX: &str = "7e99d6e2d158f4dea59597011da5d1c2eef17beed6667657f515b387035b935a"; From ae50ff65fac9f3f7ed717d4bf377b60312eba919 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:27:27 -0500 Subject: [PATCH 70/70] fixed conflicts with token rs from main --- nssa/program_methods/guest/src/bin/token.rs | 98 +++++++++++++-------- 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 8409d76..739295b 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -57,12 +57,15 @@ struct TokenHolding { } impl TokenDefinition { - fn into_data(self) -> Vec { + fn into_data(self) -> Data { 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()); - bytes.into() + bytes + .to_vec() + .try_into() + .expect("23 bytes should fit into Data") } fn parse(data: &[u8]) -> Option { @@ -122,7 +125,10 @@ impl TokenHolding { 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() + bytes + .to_vec() + .try_into() + .expect("33 bytes should fit into Data") } } @@ -376,10 +382,13 @@ fn mint_additional_supply( type Instruction = [u8; 23]; fn main() { - let ProgramInput { - pre_states, - instruction, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction, + }, + instruction_words, + ) = read_nssa_inputs::(); let post_states = match instruction[0] { 0 => { @@ -450,7 +459,7 @@ fn main() { _ => panic!("Invalid instruction"), }; - write_nssa_outputs(pre_states, post_states); + write_nssa_outputs(instruction_words, pre_states, post_states); } #[cfg(test)] @@ -562,15 +571,15 @@ mod tests { 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.account().data, - vec![ + definition_account.account().data.as_ref(), + &[ 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.account().data, - vec![ + holding_account.account().data.as_ref(), + &[ 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 @@ -620,7 +629,9 @@ mod tests { AccountWithMetadata { account: Account { // First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts - data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE], + data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE] + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -642,7 +653,7 @@ mod tests { AccountWithMetadata { account: Account { // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1], + data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -664,7 +675,7 @@ mod tests { AccountWithMetadata { account: Account { // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1], + data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -685,7 +696,7 @@ mod tests { let pre_states = vec![ AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -693,10 +704,12 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1] + data: [1] .into_iter() .chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1]) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -713,10 +726,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -724,7 +739,7 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -742,10 +757,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: false, @@ -753,7 +770,7 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE], + data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), ..Account::default() }, is_authorized: true, @@ -769,10 +786,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 37 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(37)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -781,10 +800,12 @@ mod tests { AccountWithMetadata { account: Account { // Account with balance 255 - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16] + data: [1; TOKEN_HOLDING_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(255)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: true, @@ -794,15 +815,15 @@ mod tests { let post_states = transfer(&pre_states, 11); let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); assert_eq!( - sender_post.account().data, - vec![ + sender_post.account().data.as_ref(), + [ 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.account().data, - vec![ + recipient_post.account().data.as_ref(), + [ 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 ] @@ -818,7 +839,9 @@ mod tests { data: [0; TOKEN_DEFINITION_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(1000)) - .collect(), + .collect::>() + .try_into() + .unwrap(), ..Account::default() }, is_authorized: false, @@ -832,10 +855,13 @@ mod tests { ]; let post_states = initialize_account(&pre_states); let [definition, holding] = post_states.try_into().ok().unwrap(); - assert_eq!(definition.account().data, pre_states[0].account.data); assert_eq!( - holding.account().data, - vec![ + definition.account().data.as_ref(), + pre_states[0].account.data.as_ref() + ); + assert_eq!( + holding.account().data.as_ref(), + [ 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]