From 6cbc5028cfa786a041e27177e65d7d4596778be6 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 11 Nov 2025 17:25:08 +0200 Subject: [PATCH 01/36] 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 02/36] 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 c94d353b54009acf55be5182c9af3d12db61a138 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 27 Nov 2025 22:07:53 +0300 Subject: [PATCH 03/36] 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 04/36] 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 05/36] 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 06/36] 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 07/36] 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 08/36] 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 09/36] 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 10/36] 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 11/36] 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 12/36] 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 13/36] 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 14/36] 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 15/36] 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 16/36] 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 17/36] 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 18/36] 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 19/36] 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 20/36] 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 21/36] 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 22/36] 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 23/36] 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 24/36] 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 25/36] 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 26/36] 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 27/36] 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 28/36] 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 29/36] 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 30/36] 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 31/36] 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 32/36] 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 33/36] 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 34/36] 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 35/36] 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 36/36] 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