From dfd45c8678d78eed532827160d3b14d3a2a5cbab Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 6 Nov 2025 19:35:47 -0300 Subject: [PATCH 01/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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/58] 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 46ac451284a769879c3f5894cd045776c8217e4f Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Mon, 8 Dec 2025 18:26:35 +0300 Subject: [PATCH 48/58] 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 49/58] 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 50/58] 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 51/58] 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 52/58] 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 53/58] 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 45e3223d516578744ef7b41336d47df1c97b276b Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 10 Dec 2025 10:25:33 +0200 Subject: [PATCH 54/58] 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 e9c9058827f58e6531f35fc25e94694c26b0e43b Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 10 Dec 2025 14:06:48 -0300 Subject: [PATCH 55/58] 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 56/58] 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 zcmbrn4Oo=L`ak~6>^=)DpyVPFDlWT#h>G|IYHACz_?oFHshNV{YpAH?(V|%5RWwCM z9WyIbG&3{}b**ECCis$_W9OKe2bI#y&gqo))iNXaedc+A#nbtI|JU_@uIp*`Gjq?( zJ@?%AJ@?Er*oohQs*5|6b)vz}b)g~Vx_qnDPm-k6Ve1lS4xJcHCGpD0kpd)%Aer2U zN|O_9%6q<4lEiEDDDUwBm3myZ;a&H29DVk~st}UuMWr8Ej&~e&{fiy+PNa@+**&1@ zYb@4h5S8j#rcWZR{s-Idb3Y_G#$q}^8M<5~N&Xf&NpdIKBvzCh!%mpvD3S)VO(a&| zXpxNZjS)$XZ)6Y>IaOM8>y6)ARXgIlkU9>sD8FP{{SM3W>+dyAlD>6Gm8{Y)LH+G| zlikttt9P(ie=XnfM%8lvH6~gV!Mp>fiX{yc}W{U3=Jx zP79iU59r(&S=3D>H$_`yqoit5wJ9E2WHGgrv1y@$f?VH&v!DN?s1laoL3u9pc$~vz z=$7DTh|crNk3QySk%^JXq>aTS8F{MU!(l`oIwQuGJjuXLbqS@8#fIvs3m7qiTCTICKuP1$Pg}%aRMyty0I@LdWC)7@CXxM}9{5bn1#`C&G_Y zM<^?axFmEsBeE!H<3&-2nFAEgxl5Ua8SA@7n^*Ra9|a+sOCwpou7hZG6U*w_hq|V- zimu10>v3M4PGZ^Ss1RE8C_5O{&C9A({hD2miW?*+YMdv;4@VnbCr2_l%V04bBV3}w zT1!<9i~3na?~oZ!B_e+zbG#AGmUoL3j6EC;c{w~7bM#{eyVHy}5o=+X!*{w<4Iw*Q!8f-zmmS$ZOSy^N(>(pIo_CLleg|NxpL)-WavH0ak&4VuJ zK8U(Tv+C|}nIY`^?#HR*%l7ve)K0YS$+W94yA|4x#rBM$U^=yD2(A8w<@Ag+k6P@8 z1X*cmHmmFzN~>+`Y|kas)sZ=3;;0nNmdAWZU0<^Sy&{FzOzoAZH0{!qM3IL#JJ@T; zqdO#Nj+DPkm1%Nsi)kFO6v`N&N$RkJplsM1n!K|tCdUJ1`LD6q*idRPvvIMZLBnz_ zMY7~}g0I5DM>{Z4+wf;r5Icw(&(NL&8NInV;Nz1mUe3lrekm!XW6SIw(h+YFa$WVoH^mDNo{Miuj+;w;q!$gb+m}iqK|F zwV13#72L3XsvzabEqR8%YF@+?F%>G^$Gz3Onx`$MSS>F}`(`zmLfb7)I*DOaT(NDz4@?L&P!J4R^x|pOw;s4~;WK z9`jLYi4VWk)C8wJLC{#9Z7-#_lgH)vB&Pi6gj`d8_dL`8w@#$HtvU%hH0nP)seY)r zlK^>?Aw0okh{!YLcOxcyRGzmz+H(*DgUHoJp#%rjxve?~_@fRS5zQTh$RqN@iC7{` zSRzd6tKrf&cEfrDUnG2VmXM4zQh0&%EpG?eqZ5ds(kvM&MoIZ^m%^~lNS0iP6BP{H zA&9FN8FreD5Vd63Im&2{+y-kdH5%SLW6ZBKn~(=dA@8kH{61sK-)Z(T>>A~j|E5_s z>>VW=_LRzox6jD-7>v?-z-+f2Gp85$ds{XR0b?&nRfRhp&==@|8Q#VuF@G-!^S_ud zUq8vR8mndjM&M(VECqfN^Yf7`KE9I0*9_(*2d{GX%m4aT>L;aO# z$0*wKl`yrErPv>FoLLe}QTpmilihY(vWI^qv6oGdQeG~CzE%>L0rC3eM-a0iGQo_I zNEivlNND;gpF!!RKIZsuY?8sDu17+&cI!5b2rGjk?&|K$EJFch??sYhfTaLTuJ)JG zUA{)nxecREubR=0MR4n*pirNCS2sknoz+FtEagY!5laymRi@f8 z-X62gXwS8Vc^Tm4?6z%K*rAM&hPryY-ZykR=A($s`>ja0I5V_Y0P?ygksvLz5Ki> zKQfQlx1TrJEBblacU71yMoiW2gC_f)6DE5MOwqay3Ys_3Y;bTa1ui%ho!jcR>3(tu zBw?^ypEZD6gFXC^!5+EMV2_$)ut)DS*n2n)_Lx~HkC4$JMpR=oL=udu#%T90Wa)zw zJI3T0At>lu-UEV_ZZg-)fw^i_hv=?5@yqXi%+~Drp4Og;rcDIbGEhu&r+8QuSHDW4KE&fyMMx|u6^THKs=S9o4kSd4C*pN4d^*=#@~`FlpA11GUpye*|~GE45MB1{{# z#d5cw1cNdO^Ou zn12Opy4bU$#aEKzQy>vj(KZVhP+W*I#nt?sk4W!a%Sx;dJ|-!@0^DH!Ea+94yCRb& zGU>JW@6mwY*)I*F&nk1**E}mBefda3UpZ5oJK>{rhgW^4$~=!)o*gLJP19s}=s*=PSHggZ=r5mbPF}GO!wB6#=_SJ`ZK_O8nAvICS01BI6;56}rs_;?l~JsY4pWu!w4?9J)6fMmMYSgQ+| z>(u`?J&Jh=6Ho?H{g{{*VG>M+l!~xGsss4D-L%!{p_~#N2{nZ0@pvE7vlH{wQe9-K z)A|gXm4~}CWmEHy{0P{$38rIOCZ(UPkc^dBalGyogx zvO4{Dbpf+Z4C|`KD=+0ol#**N$v+z+&Jeb7k|G7o%ZbB)JIryj6N_E*F!N8g5^yz9uQh@G3oQ~aK6?`OBG^Qpa@h{GZ6}-HzF;DLc+Z@<913w zQ@%yw<2uoZ4!S#2vi9>sPDGfX3veiSocmCZT`4Ej)mIL|ac$08~wa4A4 zjpN35SiPfv+bm_3eoW{hq5#ACNuAVnY$ezNYlr|e=b5}83n6SG51YYrP$uYmfG}Lw zIqW)!+XvgBl`F8_>F;ld@Ih!alIBOQq=v4iY5q>5Bw~4c%rQ9S&k*5$MJ+NmktVcf zl5qZ^IDmYFRgic8Tpu)U)I9borBm!On2?3iq2tw79ftHrJmzCShX!9AdYgQrn0lA^1r=pY|Ko*|M@gtbHvB8~w@{AI0xn>q-|oRFC7YA1f@G#QX9 z)(=*o%FMD-R>mjnUmM#uq6N1M8Hp`pz#)_2jL}O3-}W8}XkF+E0%oz9^2;Nt9=Y1a zBVMrhe)6Tk`H7MFjs{INBE!Wb{l!DHN`70i7dgoeoLtz8h;F*-Lp#hEgK>_27~ zWfpOG2f7AkMnGZO<>>1toZTLU0Uj$RqI#w}IpRDA?p&Fi>i5~R=7uWyC>)zmYUC@l zQmQftQ|@RvzZ)0;_sp-_VDT57d?L=>4Ec~{U(?$XyHH3hU&w@gQHWLXu(ZSUvt;+W zA?@(GfU`Cyq!)<%r{r{3xR0Smoxo#^3sy_Bv3vp(@26qmVsE z;Yw*9g;;!)QMmT8yBI~`2g;(REI#UppU^wMwjumi4{hLUCz2t2`6nbG*AOX7`H@ce z+Cs_jCVs!^l=62LN*3=}r~@%ryd6+(LhXnS?Y7UL9@`{)1QULWnH(6JkKHBX|RXeato* zYmdPO7lf)ys&T3Ddu^_|KAl{t#`$>$eok>qkK^dGN3QUy!K^#nyCx6x6?iMth`%Rw9<-W_Pkq(8F)FH@uw$TlB%1gAt)pnr@f5m?On7 zwV}Gs=z+YU28LNU8s)+;4@l|H)zGTKwFkF(zxVnw6uRjvUJ4$-~6`2 zZHDJ5eGbY9X~CA1@JBHTX_qqkph+@LweN}Jfoq}W21{`PQKfd{L(8yc5tnHqmJ%?j zl5QvIn|6v=8^&J!-yLQ3wphOT&m^|h;$o-bJCwvouMUVHDvAwnXiV-3;$E*CLwdb+}1+ZoM6>z-3g*Y)g>TQ%!8aXC#c%M z6h-ER|KS9i?4ohW4>BJtSvuuF=9L~Y&r@YCB0&WEV#!>oSa#4P-&R8RI;}0W%RUmF z3-7>8`*oT37j>GwL(n0dC}Nr2X_O4zPUBEuyr!;j28y=ol5=fQmJhLX2x;|Qe#j`+ z4$Fm7!Y-KMkxL=2Nv*`id@voRN96R^&5#yO__LCN)A7SBbQOgS!nHT+s^WEZu{r+T zEH>eFRaJGeyFTjLfjnhPsEFA0Po}unJ|m6S zUiWFxRSjy!Kf-d$eyM*rSA-{qZo{SgZpF&h%^uxbo3sRk6=LEbaD@Zcu~xDRIb=+C zA;N%BSj^8!1}Bzb95<<)KUlZ=17)rPoCdb?*Cvb4!_u|Q3Pb)bi>z!i#VSWwSz-_7 znBdF0Cv;|6DaKr#E5V^XvNsViiQw3x_1%w9?3F1Pl~Jnl z8AxuLTVJ-9wfN z=U7}Mob5;~Cn<_sXApnZEumv;_9567_VduCDkP2l9j%1h3GBb*gJUSHCM(~ff^{(j zA!t>pwLOt)QGFHRwsXpu1X&5&)QRZ*Kqt~`3Sp>|SbSE>*E$XS?ZIJqOh9|A7+#97 z#Ys$lgqM^bE@SaM01l3!;k*ab!@-0P^oU^F8zM$35$k`UFH?MGXn&l)ulysrTy18n zUvAZKQ2(}uh|_;mL8mWO8dgJSB0}c>!azx@c0>C6fME~%-2WJ1q0(?re}slBJqRD9 ztA43f!=e2RVEB*e>&0gd8!In3rbL6?5}PrmOVe zSoVN+8KGS;j&71}6k^5xL-SSZFHsanG2hcL-;wQg`*r}e{| zt*327lm^b&PMzo2kr{uX&JS2gY8;jS!KzdDdMW#D=a#DMv=@j5HdNi4Ig_+=UUsW{ zv^q5~8h(RiW;{pbAFFO=P=dayzMmCJ6O(`DIKIR*OcVhe3|U*W^?b(@7DAe^1;l4S z2swP4q;9pHziQ3iV`Jumt7)v$JR6l~GRM4kTToT-{Hde`vdz!D?}3(Q#)&cS&szN(G_Cy`OU+6V=t$Q5cF$5x zVE)?3+OU3$*R*8ITDBiMzc`tm`5WuBwX9ibz@6IP!8K2kA?7oHh8i;i6(M z(2QOAq0!Dzk%jYQ;#RCcwh65Mu8C;*UD%DC%HJoHWO)I!s|VX$@S;G$g`u>jCmT@s z(@^=QJ@U{MaUNrjI&?){ld50m`xXB*M!A+C&hgYxkEY#aQ5EIcq}c=gDmAnk(S7DrfCj+OB4TD$m;S>U>U0*AxZm$1h zsu3F%s?{4K&4o{DbyL{UjqlT>V^zyH?NYpfb@LWNlaI2auf&<1zi4AiuWEQjaTf2s z8b?Bs=kZ%2_LSN^;49{_aa(cEY(C4`I-e##Q+0Fe`-;n}cw@TY%UJ$z;x!8clVumX zx2^MV{KknQyeGkJ24$-MtqN6Mp>~{%uQ0I&t?UQviG?XQX=SU_A$Tok)3%3%nEs@k z94n!s5*Y7Q$xMG@W!p!IiLTq;TlbSJa7QZ9mG?_bQx04sR z74K=v)H++nuJ2GhbiazRe^k1mLMgre$E8U--)gG};81b=YzV@XT*?}DZp&4E2XRKN z5^K9(!r7}N6xRY*%SFO&_nhigQ@STyLfgQ~Gk^B(@$gpQN1#I^RP;kCSh> zKBYXOLY&kpfuuUIF}S>7)W%W9!gmjRRBdM`lk2rpJLx;cx65L0qo~(St*tjfBWnj6 zU%S77YuzbdWm|U-Y#Vj>ijb2^*Zy60eRrD5`zFT5ytTrv^davRM*(Vm$6v(qW%1Hw zdgB##^R39BL+>y-RU?tk#NWWD@<`KF7QQFJ61D4!xS-Rjo8^0K)C(N!aT-v}uJ0K{ z(Yf_)+_*elHTmsiYTkHS9c%4bwtH_Jt@*U7es3RQcBZM{!pxUgr+4O4=NGKxoj5us zja9$XhbCWSci#CMHPy3I`(L77AF?qAriz7Q_W@go^MH`AW(e!WL5|u!*0bvehSH)- zEd1S}w9v#d-u*Df`B$|t`2rMysO!2kK^0PAwHp)7ii$pppMy#D8h(n6tBR(DKUU>b zO{eCXdX4XI+4Vy)E$}AmbU2wp>KTV$3N5;;Tv-yIB*kYfNpW2jTDy0d(fypTb{xo% zkfI;>Ib|+KHoFQ%&x(Fv^+)>9`0iDK@6D#P@aL+WKjX5Za9&3@!T8)xiOOabClZ=5uVqoko=cWZ9sWmr$r$!L z;uqxz+mH+e2#S<6eo0@`1u@&8F-eBx_Am_Wpe}?#MztnYMkl@5&0`a2Tp)}6IK=$% zV`|N1?O5t@n_Tn|K2)$rk9T0N9}h4WP1nTg&yE~_%u9{D)-b=1ZFE-vs>ihP`VPFB z)h1?JSmwvw+J0dbA5;q5m=N5t@?{kt!-=+K=RS@xdwrwjnwj^Bm&_*DOKi@wbUyEYszR0EGz&24?jVry*jjkX#cTUax^?Ts)vp4wd+#U*y4W>VX4Fy6wN zU7E|h#G-1Wscd2sYvXA9_H1=+BaK6HvUdlQBAC2*FQGgqk89kuuR4En0u6CqwfJJQ zjIC*U>8x()o8<6#)vH*UHwE4S8!6$b+mhQZgZZsR?zk~HVPva{gu0@y+-nX0Hy1_P_?Pf6f++fvbhtis- z)_?kNTUEzcyrSY>Gg#E8p)~nPHsaHn?NaQ*GYLzqEunHZmCTj=hh%kspB+?t zgMsHAZO=F0#QH}1CX=__WV*p7pNDr44cYoQAI2GJqjsC%_bv|sEH~*FQVw%{D|6bEpmH_#kI!~@aIyMC@(dI*n8}{VUIa{L(ti6yVNvX#rcu7 zR}GOxSJ~Om_xi@jSL{9HD;Bx%Dl51!q+{i-EB1Ydui$JS_W@2z*QorHHk^K}?!t6h zc(rQq7o{Xb9&A~5zb(VZ*z};o%IbuLndV132Upc!yh5owfK~q^h8D)LTS(;o%v$%8 z$PB!^j}{JN-(STxAc@_)JXDlLUGdCpxB}N0#|B^Z$gB#e-}JwBTKyF@mWf4w9;Y2Ck4NJwo4FBcBjK@AZne$S84b)`0gjrsQ|YMRS7 z|NBE)dyP&0Av7fUM*gm%8-3?ofJ%5#ab?L6S^RB&TL0v+PCq^95+yZA#NbKy1B)RHq<;E!V|^e^MbK7)#` z|SI zKfXwv3)u2oBRiPkmP*g$#n-`=`i{|)hAh17jGflHZ5dZf0|CuEMW~l-Jr?K zs;>XfB_Aq(!>WHzriEWK@5WH`>96g^h->y;K3}ug#z@IxSh3y!2 zf~-K0jl9N|Hzw2C6|A~(Ur^z7#UB;yyq+Ft@^(Lq&I_G+tf1*|P|bB_3c?Mr&#$K^ znW$(4e)jstapbm_LROQBO&YWhF4Z8viF{ zB`D?vCbC!UvSytymHB3Dx|gBQl)TAZ=|$40R$2~k*I66RHa@^iY<+4JhrXlji3WSo z&KvMY%13eR>2Bn!7aNoh$|44HBcy`_|4{MyK{JUn7hiGXh5{@0y2S68$pWpT?R>5; ziBQ3$9ejhY&g|#Axz&$^@R@!jJhbp4KGOT)lWZe~+t_?`pp2sSZC>F=YMVz@yO(GC zlNjpU&nx_ijXDpo*t=o;JAaag4Ia;IgEILb-`a-k8sz-5#gq!8Fo`?7k@zG@FkOM- zkW$<4T#AP{u`1`OAH)MBH^j<{Prk=<+L9DpwRVFCR%AMC!5;#sWnON6vn?6lMX6Y{ zS?nUUxj4c=6pu;801l- zFIep)caVr2if+5d1(H70Ty$9-QSBAJI*4qcJ|DWHI*_r1%BS7SgGniEQ&gjDCluKi zo!|i-34*_mc~nO-ijFwy&hAKFH_+lU?!YeOIEidcZ7KAEad>O7smn{%L)31t68vS- zCwy`ksij_jb6aglsn_4#H^WIGX&WKJAelFdCnu6bbbKJQMMhrd`O!#%K{Yv3oh0vieyb~n{P|ZrCyK10 z#W%TMH?&v$Pj_rL@*Jhb-?=Nhlj#Opc#He>g0!6X-Mf1cGXm#Z{9tdg9vOT@A991n z-Q%%+Nn~W?y>#=>eum^=DL>g8;ck)CclHHIWh>}je%BMfqTI~O`;s7YZIYUAy2rQn zMYliy!mE**Zv2vN{Mo3`@W#HxJM_XY>GWrW+2g#@+ntQW9|QepK;zn2e{qNRBWJ+T z_xx5I35qUkPz#EF(}d8Zevm!b!0VMA!Ha5s@xOU&Jc$e`#>p4X^G%YzUJldBf8<&5 znEI2qc}YA8olyKQL|_KB?Bq`t*$*=&J_Y$rc-9uYAK>U--(^0PZ*0NI7W zx4*i|R)40t&zj!xs^{y+!yh4$w!&ZFGJkVNDke~D@k(lPBY=i|AJ;Ts2+w>3RwX~m zD<8oW6t#7qdxX48Xl-X+Fa)ZztTV410`b;_x|a_n9}?;n!v{FX&_QxHZSta2w!L~X z`5gWf=wPEJ=5Mw9NHKK9q7;Yi)s^pekWlIs;XdaeGYwRZ<^7V#Ai6P@XC=WXcE<7n zPLfDXy?Blj7JE3>z28Zqah4U!>yk+yvvZs}#@ZAfJe<5wy@t8Z4<|nusaG;TF@_8c z@k&yp22*f)AdI#sAPqUOt&@X=$n5AGee>#iOO&Q`DADO@aNRrD;=1 zoM>soRPws$q2E*R2S__jBNs$^XBzn@Qg{8+gyORRge&0+R*O*io#|wgZOkEe4m5MM z--!4XSU`MU8R9mQj3ld)nJT(ik^ zk=D<~IFTPbhYS>Fb%+wy^2u|cqVj0IdJa|x5M7)@HqpBydDdLgCq~AfS>X$5suBm^ zOgG9_n7_?)?228efzJqjZZ6ifjUzYBLzCd6-#pkK1~6?Nw4whfzIh($)3)e*%SVzSm{C1d%(!XWx`+V9L>uzNnDUT3J%YzDUHTO`_a5E zlPqjjDo6L$ZS)__GZtX=a-xj|BtaFWT*fahATI?e#uU=JLd|HNwGf?tKAP`e2q*Q; zXnua7Y9RrOFx-gIJarLbEJZejtNH3hWC=#IDGPP(j^>Um5@(V};40-kZk0(CugD@V zg+`A7=W;sr357bTqilodPmyE)F+6E8nLrD_B3MGM&s*~O%;zUIkeM;HcW^{ZwLeDuD1H;%sY%TfA+(xb z{ehnEp16_t`*%S{pnAnpC}J=D?ymGNor=4*lZOR2All5!w-Z}v&0W2a$>li47SgW1 zuIKUk?Ib8y>lVS8dN)`4Iv#_t$o*HvD{YxzRn=Nusa`FU*Jx#BhlnYl;ygr z+p2}ScxvBP#G`iDCVrzbSHJ2^BiPp(b@*vw*scBT0Rpc`s?K3bhUdY#GY zsH?rSf;aU7%^pp9$IWxlx}mp}zoHP@s^nu!s4D0#-mss%a9k8jZTE6-ad;&JB#=`_(SMj6L zO+wqyk>}iJYv2Ls#;bhm6%v9&P`B$82|}RMl5y}fneLgvV?RLvcjzkbbQT`B<|-d| zR`|BdyyPsr<*cjx#94UgyI1+0vv3OET;;)^!hf}|=hHqVE5zD|PpvQWx=%$^^|`y@ zQ_^Ul!@uASpMiS#1-JEY%|&!+3o!mi8IiWdbi^`nE)G1c5nEWT#<86;?r`$eT%8!S7u7W z{to-jSYbQWNvfI`_E$9|IbN|3wJct&%d`StEGx5T`cPH~sqCVMdN)7<_3hx|-Zngub@V?ZCOeLE+q z?G-(Vh=*$9IjZqBK`Wb#bzLi~eqZbVY3{m9f8bo3)#~))`|o1=;+5v! z{4=>kLcL}v1ChmFL5lsPa^_j8S!gORy9ZO4wa9(y9 zS-{UVB3{|NfCn@o1}R*?bDGH5#Qqs-f622I9<4$F9D&-{=4fT}n0yOgQAL@nomMtq z<1_P+AKgS^{;`BneE7o0uk;cV{>;N+5PyTkWE{gOZsx`k1Z}4HRd6W2pDe+RbQ(i9 zEr0{C(V(R~oYI9fc`4sO>841h)^zOxRM%H#ZAq(Hs5V!-*geib*OQn_ODqN)Xn6E1 z{zCD=e%+-o{?ao`cz~?*%N?@Pu{%Si*wCZrG84tMg$2CcL~%7{=_W54(#%NFQV&Mn zP{&`GtukWz6L)x_O?f%b@}fQ424R3EQ@I<1Mkw+ej(uq#BVC@$>wW3a(3;~GQxeRr z;8*vzI0-2}$t>Q+`|iykE;;4mgZ=2t5J7|T8{(v-mN75m{dkn)W>P6X;z#d`whsE! z#zFYUC%jJFkH4s#V2@|s#^2mFwDS_ZV={$x^yI$SqgE;Q%LnA^|mef6f%X{sYmU8T1#trRsfyZF51 zh=A0dcD3Z;15h)qv_s4PQOOGsnknhDw_5V5D>S;>s+FNHh1Rjd;vU$6&NGD0`bDEa z7>M{u2`s8->v_C7goXrZA)->|nA==-D}>^E)g8BYN4f;(RCnAtohTMC1hX4DLnFSq z?>^C)QYGV5D1Ex4`h}Tcb{o{$HLcU+^&59|p()h)hG|^m>p>k6+iznG=&n_)p+|#?Tco~1yBtMdu+32LM8c}?$ zIB!q>0NQvj9@>a5)JClIz5oYll#!u{Uog?cea{gFO` z6uTg)*U}~Fz0wygnWg;5TC!-)lC%XeOO~dhJQ(HA@zeciM8>?O*6c-#teI&`=FAc` zX3ksKDlZStY+Dt8@}o2>qA%alK!-xRjR?i8x(^YA0fqov!sn~#-cwhoV5gjz#9YT;(&Pz zmu4?{Ha&aZqJ=ZWhA)~t-BS&Not&sdtBmOX3f0tkMn5WPgl z*d+Yzr}BtVi1%K;6{k=GX$B1+!)pi9=lnN=DitNS31q|bhS7ff-(#qq20q4j4Wd)q zTuienHI>>&g&fxVH$7&PGEip>-ZnyElKFEgRg1^$=8)Rwzi%hxrx2e8Q7jkZqAu}UX$tkMRgN0FXIIuWlvNtvMY z#AAR(-gT|+J+1CRt*-TXYfOywh4i${%vl-M)CseeKFb#kplhH*N4!V zxf$bBDKX?fqOh)xUXjXTtE88&N0}Y3j^DuVE*^M0e)q(yKM=Z9Kfk5ED1TBbv?3Ml zkK1Z(D`kNxp}HbtZVq2Fl*ZAr&AfIf9hK`SY%K*(tV`kic$+l12&#?IcS0(pkc3n` zy0lkATAsKEu*eITpw<=A7j=d7McvE-s}v>SkAMVIryvC_L&2MYMcx=xE=OvFBj|xj z6&Of0@H8+Tj(U1sPpsE1(dwQ<9nZSfXROj>w3Ur_z#=QSSb&U$R!PdVN+Xat7jJ5o zRk{egd;yPf&?4ID34Yi?=hM|sa9aY6q<5a+qY`MGEfDuy#grs0UHa^TEMaQ^cm!kO zP2Iw`Cs13vTV+W_=NC385o^|C^S5pG4l=+oD`0@4!dy!@M09ZK;pYVBuoJ+5ALyTKY5AnfOoklC<<$ zC|&b3zcLCT;#*Jin9;PfT^w$bCxO-v+S?gv+;o0sG@aUYEAl->HUPZpF5;*gE&>?* z^T!9#KKxv7YUagbsDIah88*p@W>Ub15T1Af@PbpRWf)4eNue$fk4M30UXtQr2U+nU z16VxQXYiQEXk>1|39D3IYn3FtgYizRfexRvN=NYKi2PGlsSbH{z_Cb=;N5`Ven^8+ zeiQZmfW@O5=-ktg=NZ@(%4V*}o@Gs2vLtQA%tebbXQeH)rl!7>s(SoYskSE-hUP8I zo`n_AnujYd1S&AvW_eqP-tH-eSpXb{jr;+V45ex|@`zqc(cr$16OOMk*bpK2qG>#6H zucLf`+GplC>X#CpZj(BK?ixC?Aia5|MJ3LCjXo|z{K)gn-+c|DzGRh39>~u|epDU2 z%@zKaaWu5uX0$DM?toW}xXEL*dh`kFK6rOy(wjh)5!;i6;jJHJ4YziSyu@W0?( zf$!Y3N)skMF>yv>!m!a3CnOAmT4#*3Nme0|4m<#pMNiD)(NEB+KGJN>Sa_Z&JNP&a zq|;{e_n&}^sGQ9kpMZZUo6SQe(&&&|p!ES~zakaJ>ov!waOpFLPoGGKPVWUQmd!bs zy;!9C@3ruD=QpcV*@tXuIxLb=;o$6=+L5-gu38D}LdnlW26iRNN$g7Je_rp>1rpKrsTjU&|5%E=itPOv|lj`1&a{u}xM~m=qfVE;@#ZysxLwW$ix9 zRDQFbUf$*9QOd84J^z?rx#JUFfScv;kR%lI!0)%xFsEnok55QVFsZmJ@dr} zDjful#H*Jd!fzoU9hc(wNDsUUzq>qudwJm3@LOn7k8V*z2S-LbtP36L=wA|<9)tz> z9f3E-Bd=J?Ln4X!6>oMlZ=6PBnx!a4DQ5Q0!Z2w}Rv1E|Feyh%%NMIy@3!UjHN17y6{*P>Rp5 z9@q{n>g(m(S% zq``Raf%YigXf0pR%J|)0lw~1^)bf3icEsBa@42F|=Fls!7;X!1c~MxHSA|KHctIfXbrTRT>>G_w@_x(IwfiPyAl6v@0hF><@)y$)fxo_t|CCN`=2M^* z@-vQfhi1@!5?ZjFn`h(T1L>gIG}@;C7ZSxBFXs;}rty7r_&l=U10@|F$8WLH>G(JN z_E#{!FdI$|&4tdP$+>#@3Y2-)_XTsF*zW=C{{UVB9PZKIpMV9Mdiw>y?L6>oVDMva zCK!VZF@1W4w!kU_Zqr|jl^cs2H$p{|k&2;?IT9vi z{~xdM{V?hLdo6Sg9>4#ux(}51{;!<{qLT-RrR(U3C(Oq>HV)tNdCq)lZI|?dswb}? zeiyzv=>@)TK8^M+K)zV*dD=?3JNJEt`kOQU5hg`lfu>z^+n%A*W&hxO^(^KE(6#4Z z&88hqI6{dH&gc8Gpgl;hWMO4RYF>1=9NIx?cnR$z(if4o(+MwY(~11!OK98#J-;t)-BBE{+L1b* z%m=WDe}!!H`ad>fK58kQK&?gm^`#gv(!VUFeL}4`Ll<%O2YAH*V`5_@J#Q}I3un`~ zNFC49cBb)k&#`k2?7eeJ4sTj;|oJEP)>1kjSQqX<)EDa6O=~g3O zXto(Cc$Dgj`OnY7IiCfN0LJPsiKyaiiF@cWdO=S7V2w@jwu>=jVKbgsczGRXpttrO zczrYGDLIg0kF~rghc5KqzgCSi-Q!ltKT`LTLgcSPE(=L9@J$#nNK69~8k+*42_-a?4=AEmG&H3M zbx!fTBD7b>FBeg}JlKFOwD{STd@qtNJhg-xO{FsiZX7JvEx?ZhXoq+RIE(frDK8tNk?(Ey#e$zNMZdyxSC=}Ov_4j99) z?4UL|2ea?solC*kI6l6Vb|IO3aVd?byC362chWwx!wXS1{|Z^8nm@b>9R-eSY&yYv z7h~gaRo?JqV^b7gS4KN3ZCT+OH}GR9A-nl^FN4#wyz?4G z^y87pgD1Bl_d^^$nD1RfyP6y*$tL`Vwe(@Wwv@J)Gf|eq{nyfX`R0_yrX&2{Yay;# zYiUQ4!SmMAN0A&`i;hzHcWdb*^2Mh_(+kAZ4=#F82mx25! zKU4;p-Flk;PzFbMYdW{B!&a-ujK-!|{ux71kh6|@lfk@j9j0I~KeUc^Cl~ow>o8hJ z8kkh4Gi*Jkvw#m=k0#4_+IrCD@OA4!Tgg9IkGb*VmqnQ$ml+*Ky=SX4EPU3vw%CgC z#Y||{Y`&%p6EF4`J+KAq#6M-Io5cUk(9%}^@CGzd&l5I4b_3=#HU;zXo511f4b)qX z1zyc@0nEZHH$c}5_@^L4CEQf#DsP?J*wll6RfLHVjqk^5QO?J2MC0Ln@kUH%2LE6q z45$bHRb(ddhc}^e5`S!yg7DS0E1AqUZi1Y1_~!yu^52ojm5UmiHuLfOFi(jb-@*d; z6pm7um`Kj?w>a%3M=ch5_Sqg9$qk!n82<1*Vl#$2Ih!xsgyD_>NA=hbX7aqv^l@C0 z{A@Egf~vfNQA>Q}D`;r|U;GLj-&i$7M5 zZmc{{pcDLqa!f@J{(Cu$FOVl}!+7WNb=y!qfM4E5pF%4Gx1;(|{_=K+WFoKGjxs;~ z&<-R8{OKJump1%Wc#=qJJjX*zX*7?mfJX~Fr+SZbR!MU4#T77)qn|0|inIrZVd3JJ zD->zVJJGa}4?$we`g<#jeMr$WKbx4Fy*r^q=lG{PF@R|te>#AFN%lgsCmOd4o~RBu ziYKg#%ZF4#gi-w2O0*uu->amfO{r~~nsATohyD1T z7rmE;%4gd)>B~skUMwS0fYRU&+C`S4v5NC;dud<49>}#~36hf9r1IOZ(lCyDq;^At zrHMbXo_6*Tie3qFZ~oXm46K~z?Srs#_+Eh={HuL%t+5?6R=ddM-4O}#gm*A_2cP#2 zNVE7xk(Bde??65$_*d^>BBkIaZQ^+BehkhVIF*MTf*TsMpL&}{0H2`z!%EDa3nRk1 zv28z0qesUkxUaM|vX%QEKywoBc>s-P^C<_Q-{<(o1CWa!|MUR*59D_b(8o=V9_Y%8 zzy3A?yG8HPp3wBy-i6rq^RM2eT@BOXn)uZs4Ag;vmSH!O!FyLJ*70l=8tK7btHMAt z_-9p6hlxDwARP|xIPV}xw(`9|a#CVb6a363%}M6;mZXWi^C57znh!YyIyk69;4Yhg za0mt3|q|N?;QpU%lTJ_(F(ku z8z`7R=0;;Vd?gb3;KU}ZC0~`v9eBN)_J9NOKZ1@2^PWd&cUmx+H?EaC$~f&To6JMY z)SpKt^P%|ZjguYYl%}S^{1~ds0|2f3%OliIM)2Q7BV%~y_n?QF)0&#H_(MmaK{r8j zl&^db62Hz5y$5b2e)&C!rw4!NeV{1!!1w7jLa!(C(4&YfCnxf;M{$I>Igu|EX?p0gWC%-~!0Jtx`FnI9$k(e7`{BzW79k#a7@qfd#WzWFXH=chlQeJ4yFrd0ty?XZ`*2Q2Od2>u2GTa(lyS%K#O2LLNg0Ivhy z>Vb~}j~K?sen?-Zfy4L@A40CuaNg!4dWBXd@gF`yenS#pQ;ox=JBj>CHO~I$;$8#7 zKBd1P1ZtC=yv;Gx+vVgjB6T_WSdp%G@`WO;!@UQQE_CvJB0b{de?i*r9NIw)q_ndR z5kA^)JVs;M|ES%Y5d6oO;ils8J%T43ryX-MlGQL?6cwXrBCtNRcY$wt;LE_vhqsh_ zBY4{kES5S^zb|l!NBMN%vmSUQaFR!R2Yqo~T0O*y>f*saRRMx}57~bO90;sFRJsnV z-!Ty#w!vTp*2^CP9^rulfyLbFjkaPAglh-)=E!E3-HJa9SiWDmRpSg5N`zYBPw2Yw6K<$)`K z*L&c1fOoavTuwE`U1w!d` z{3EbXa`nlj(oe_`R-hBy1zroBh78f56~|Zt>*aP}VHG-#6vtf9avgUA4)nl1fV+C& zUclmllJexDgFeW3M9%>34?MyH4*=G0DhY;%A)b%O~d8q5l?ipN}J)Og$K_3 z9MjbUpFwqccu}apFcfQ*tQvj`1;Rqq z_MjidU#S+McQE{#>O>}^g6N}9b^NG3w(O{3gDq09aI2YJ<8t#E;#Nj|Pt^ zNC5U5VMD2SsFa(83~}2~eGrZSZ}q^VfDd}$$ABwvu&C3I13uw_p8yU91A2K1u$WRE zPXQKFuHjs18Zv}Pb%GhdVh(jY6Ijfdj%NW29nkSyU?FlHKLad8tmB2iVoG(q7+Aj$ zNhBEYRxLv(cmr6!IVqIvjE42{Pl1Jqb$kvuQ$slWl>QFf^4xBE?;BO*-H|Db_&%_h z56;gX4L4jfQ6Yx z0SiBS1vm-VNbpfwWIXwus&t)@A@EUPp>$Ra5Bgr!k(pY9&jX7&AFtt~z(=4XdWRQ0 zBw&J)|@jEh%I9*mBs(f+q6sh4Mtw>33H&y>zZE%{# z;P4;R`tVnZh>!iKb}#}>i2<2B>I;Ut0^b^C1-0lO3`8U2)EJQ=9wPMwzCPBfAc12A z_F%9t@DCn14)_-jJP_D(0E2;Bp4&~c|Eacr7W$`fIPsRsKqn1f1r{ADo*p_8b{lpN z3_&R){2f>@;G^L$e^MDh6s(j#>d~PXaVoGUL+M(%GJaglZJPZ+g<|0S~;JVYu5 zUOmC8Ac0>7-ta^VE)#fS3ueHnDJ^&tuqOkr0OQg62LmfHhoXVA+ML=khk^mD>57EX zZ>tSn*YKklv0%tsqwn}X8bcc1s&*hcfnwfnI{E+uA1UQ}{~$R2K!tCB{l=&teuN)r zi&dutSeXLgLBL5*T9pojM1}w#@xTsX{d%;hKVQRY1u887_RB?sdPWv90zL3j;PW0z zwG22Mrp|7@Z19p z{@tTo=+FfZEa-m)Hcqi>eG;kRf%;ADn&)58+-|ysITRK8X>&*|T3sZf!3f|1Q>|Ji zk)nVPPH(~8fyG^O^}&V~_?`#u4Q#}>JiWZ11@j;1kr5AqxgK~B@Ny441lUug4&avO zcGILgDuX$2g~IWC0xaqa)e^qn|E|iApN1dLMTY257x_A1!J&@-4lG6(ZcrN>`?EU2 zRIPzBU@?I48Xo4skdWBy2N<5`Q7$B&o9U4uDy#**m}W(D;vv#{;QBPH0C^Wuc0k=H4x0?q2s*XT8Wx*Uh4=hFi9aQG*D6klTzPkMmEJon1 zl@GnIN=#re=ZU|m1Jb9k09Xu2*MVAKAwh?j(p*%CZfL!}X96c-d9`XC>;o1f)&q!J zz(OK=2Yq12V#GSm2G;+2LMYu~-~x~Kj(SKSSIqfUWQadVP#+?F4J`i3p%XGF_tNRR;>g;!mias5*?n1X6vtd>Oy*}<8(5J#`2 z+0z%|Ih7frvZR8G@LHYqGKRy$0`(18K4qo5Wt-2wJVLA@H*pkr+Ct7 z{cJ3gPTdB}q*L?5GEBO}Z9@uVl4wFZER#�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 57/58] 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 58/58] 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(()) }