From 9788f189b195cf649d417ab119002c166b4e675a Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 4 Nov 2025 16:09:04 +0200 Subject: [PATCH 01/90] feat: stat of deterministic passwords --- key_protocol/src/key_management/mod.rs | 19 +++++++++ .../src/key_management/secret_holders.rs | 12 ++++++ key_protocol/src/key_protocol_core/mod.rs | 42 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index f22a99f..b642e13 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -39,6 +39,25 @@ impl KeyChain { } } + pub fn new_mnemonic(passphrase: String) -> Self { + //Currently dropping SeedHolder at the end of initialization. + //Now entirely sure if we need it in the future. + let seed_holder = SeedHolder::new_mnemonic(passphrase); + let secret_spending_key = seed_holder.produce_top_secret_key_holder(); + + let private_key_holder = secret_spending_key.produce_private_key_holder(); + + let nullifer_public_key = private_key_holder.generate_nullifier_public_key(); + let incoming_viewing_public_key = private_key_holder.generate_incoming_viewing_public_key(); + + Self { + secret_spending_key, + private_key_holder, + nullifer_public_key, + incoming_viewing_public_key, + } + } + pub fn calculate_shared_secret_receiver( &self, ephemeral_public_key_sender: EphemeralPublicKey, diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index 57dea90..f05a641 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -44,6 +44,18 @@ impl SeedHolder { } } + pub fn new_mnemonic(passphrase: String) -> Self { + let mut enthopy_bytes: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut enthopy_bytes); + + let mnemonic = Mnemonic::from_entropy(&enthopy_bytes).unwrap(); + let seed_wide = mnemonic.to_seed(passphrase); + + Self { + seed: seed_wide.to_vec(), + } + } + pub fn generate_secret_spending_key_hash(&self) -> HashType { let mut hash = hmac_sha512::HMAC::mac(&self.seed, "NSSA_seed"); diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index b1ebe71..33a007a 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -14,6 +14,8 @@ pub struct NSSAUserData { pub pub_account_signing_keys: HashMap, ///Map for all user private accounts pub user_private_accounts: HashMap, + ///Mnemonic passphrase + pub password: String, } impl NSSAUserData { @@ -64,6 +66,31 @@ impl NSSAUserData { Ok(Self { pub_account_signing_keys: accounts_keys, user_private_accounts: accounts_key_chains, + password: "mnemonic".to_string(), + }) + } + + pub fn new_with_accounts_and_password( + accounts_keys: HashMap, + accounts_key_chains: HashMap, + password: String, + ) -> Result { + if !Self::valid_public_key_transaction_pairing_check(&accounts_keys) { + anyhow::bail!( + "Key transaction pairing check not satisfied, there is addresses, which is not derived from keys" + ); + } + + if !Self::valid_private_key_transaction_pairing_check(&accounts_key_chains) { + anyhow::bail!( + "Key transaction pairing check not satisfied, there is addresses, which is not derived from keys" + ); + } + + Ok(Self { + pub_account_signing_keys: accounts_keys, + user_private_accounts: accounts_key_chains, + password, }) } @@ -100,6 +127,21 @@ impl NSSAUserData { address } + /// Generated new private key for privacy preserving transactions + /// + /// Returns the address of new account + pub fn generate_new_privacy_preserving_transaction_key_chain_mnemonic( + &mut self, + ) -> nssa::Address { + let key_chain = KeyChain::new_mnemonic(self.password.clone()); + let address = nssa::Address::from(&key_chain.nullifer_public_key); + + self.user_private_accounts + .insert(address, (key_chain, nssa_core::account::Account::default())); + + address + } + /// Returns the signing key for public transaction signatures pub fn get_private_account( &self, From af1e129b6baa70f7c25ecc8b4efa66e4f8de80f0 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 5 Nov 2025 15:15:29 +0200 Subject: [PATCH 02/90] feat: public keys tree --- key_protocol/src/key_management/key_tree.rs | 205 ++++++++++++++++++++ key_protocol/src/key_management/mod.rs | 1 + 2 files changed, 206 insertions(+) create mode 100644 key_protocol/src/key_management/key_tree.rs diff --git a/key_protocol/src/key_management/key_tree.rs b/key_protocol/src/key_management/key_tree.rs new file mode 100644 index 0000000..8f3772f --- /dev/null +++ b/key_protocol/src/key_management/key_tree.rs @@ -0,0 +1,205 @@ +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, + u32, +}; + +use crate::key_management::secret_holders::SeedHolder; + +#[derive(Debug)] +pub struct ChildKeysPublic { + pub csk: nssa::PrivateKey, + pub cpk: nssa::PublicKey, + pub ccc: [u8; 32], + ///Can be None if root + pub cci: Option, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct ChainIndex(Vec); + +impl FromStr for ChainIndex { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result { + if s == "" { + return Ok(Self(vec![])); + } + + let hex_decoded = hex::decode(s)?; + + if !hex_decoded.len().is_multiple_of(4) { + Err(hex::FromHexError::InvalidStringLength) + } else { + let mut res_vec = vec![]; + + for i in 0..(hex_decoded.len() / 4) { + res_vec.push(u32::from_le_bytes([ + hex_decoded[4 * i], + hex_decoded[4 * i + 1], + hex_decoded[4 * i + 2], + hex_decoded[4 * i + 3], + ])); + } + + Ok(Self(res_vec)) + } + } +} + +impl ToString for ChainIndex { + fn to_string(&self) -> String { + if self.0.is_empty() { + return "".to_string(); + } + + let mut res_vec = vec![]; + + for index in &self.0 { + res_vec.extend_from_slice(&index.to_le_bytes()); + } + + hex::encode(res_vec) + } +} + +impl ChainIndex { + pub fn next_in_line(&self) -> ChainIndex { + let mut chain = self.0.clone(); + //ToDo: Add overflow check + chain.last_mut().map(|last_p| *last_p += 1); + + ChainIndex(chain) + } + + pub fn n_th_son(&self, son_id: u32) -> ChainIndex { + let mut chain = self.0.clone(); + chain.push(son_id); + + ChainIndex(chain) + } +} + +#[derive(Debug)] +pub struct KeyTreePublic { + pub key_map: BTreeMap, + pub addr_map: HashMap, +} + +impl KeyTreePublic { + pub fn new(seed: &SeedHolder) -> Self { + let seed_fit: [u8; 64] = seed.seed.clone().try_into().unwrap(); + let hash_value = hmac_sha512::HMAC::mac(&seed_fit, "NSSA_master_pub"); + + let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); + let ccc = *hash_value.last_chunk::<32>().unwrap(); + let cpk = nssa::PublicKey::new_from_private_key(&csk); + let address = nssa::Address::from(&cpk); + + let root_keys = ChildKeysPublic { + csk, + cpk, + ccc, + cci: None, + }; + + let mut key_map = BTreeMap::new(); + let mut addr_map = HashMap::new(); + + key_map.insert(ChainIndex::from_str("").unwrap(), root_keys); + addr_map.insert(address, ChainIndex::from_str("").unwrap()); + + Self { key_map, addr_map } + } + + pub fn find_last_son_of_id(&self, father_id: &ChainIndex) -> Option { + if !self.key_map.contains_key(father_id) { + return None; + } + + let leftmost_son = father_id.n_th_son(u32::MIN); + + if !self.key_map.contains_key(&leftmost_son) { + Some(0) + } else { + let mut right = u32::MAX - 1; + let mut left_border = u32::MIN; + let mut right_border = u32::MAX; + + loop { + let rightmost_son = father_id.n_th_son(right); + + let rightmost_ref = self.key_map.get(&rightmost_son); + let rightmost_ref_next = self.key_map.get(&rightmost_son.next_in_line()); + + match (&rightmost_ref, &rightmost_ref_next) { + (Some(_), Some(_)) => { + left_border = right; + right = (right + right_border) / 2; + } + (Some(_), None) => { + break Some(right); + } + (None, None) => { + right_border = right; + right = (left_border + right) / 2; + } + (None, Some(_)) => { + unreachable!(); + } + } + } + } + } + + pub fn generate_new_pub_keys(&mut self, father_cci: ChainIndex) -> Option { + if !self.key_map.contains_key(&father_cci) { + return None; + } + + let father_keys = self.key_map.get(&father_cci).unwrap(); + let next_son_id = self.find_last_son_of_id(&father_cci).unwrap(); + let next_son_cci = father_cci.n_th_son(next_son_id); + + let mut hash_input = vec![]; + hash_input.extend_from_slice(father_keys.csk.value()); + hash_input.extend_from_slice(&next_son_id.to_le_bytes()); + + let hash_value = hmac_sha512::HMAC::mac(&hash_input, father_keys.ccc); + + let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); + let ccc = *hash_value.last_chunk::<32>().unwrap(); + let cpk = nssa::PublicKey::new_from_private_key(&csk); + let address = nssa::Address::from(&cpk); + + let child_keys = ChildKeysPublic { + csk, + cpk, + ccc, + cci: None, + }; + + self.key_map.insert(next_son_cci.clone(), child_keys); + self.addr_map.insert(address, next_son_cci); + + Some(address) + } + + pub fn get_pub_keys(&self, addr: nssa::Address) -> Option<&ChildKeysPublic> { + self.addr_map + .get(&addr) + .map(|chain_id| self.key_map.get(chain_id)) + .flatten() + } + + pub fn topology_hexdump(&self) -> String { + let mut hex_dump = String::new(); + + //Very inefficient + for chain_id in self.key_map.keys() { + hex_dump = format!("{hex_dump}{}", chain_id.to_string()); + } + + hex_dump + } +} diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index b642e13..4c3d5e0 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; pub type PublicAccountSigningKey = [u8; 32]; pub mod ephemeral_key_holder; +pub mod key_tree; pub mod secret_holders; #[derive(Serialize, Deserialize, Clone, Debug)] From 4a430622f42ca202bfe9eaa410fd26c7086d5b60 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Thu, 6 Nov 2025 16:50:32 +0200 Subject: [PATCH 03/90] fix: testing and debugging --- key_protocol/src/key_management/key_tree.rs | 429 ++++++++++++++++---- 1 file changed, 357 insertions(+), 72 deletions(-) diff --git a/key_protocol/src/key_management/key_tree.rs b/key_protocol/src/key_management/key_tree.rs index 8f3772f..dd59363 100644 --- a/key_protocol/src/key_management/key_tree.rs +++ b/key_protocol/src/key_management/key_tree.rs @@ -1,20 +1,10 @@ use std::{ collections::{BTreeMap, HashMap}, str::FromStr, - u32, }; use crate::key_management::secret_holders::SeedHolder; -#[derive(Debug)] -pub struct ChildKeysPublic { - pub csk: nssa::PrivateKey, - pub cpk: nssa::PublicKey, - pub ccc: [u8; 32], - ///Can be None if root - pub cci: Option, -} - #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct ChainIndex(Vec); @@ -22,7 +12,7 @@ impl FromStr for ChainIndex { type Err = hex::FromHexError; fn from_str(s: &str) -> Result { - if s == "" { + if s.is_empty() { return Ok(Self(vec![])); } @@ -47,6 +37,7 @@ impl FromStr for ChainIndex { } } +#[allow(clippy::to_string_trait_impl)] impl ToString for ChainIndex { fn to_string(&self) -> String { if self.0.is_empty() { @@ -64,22 +55,77 @@ impl ToString for ChainIndex { } impl ChainIndex { + pub fn root() -> Self { + ChainIndex::from_str("").unwrap() + } + + pub fn chain(&self) -> &[u32] { + &self.0 + } + pub fn next_in_line(&self) -> ChainIndex { let mut chain = self.0.clone(); //ToDo: Add overflow check - chain.last_mut().map(|last_p| *last_p += 1); + if let Some(last_p) = chain.last_mut() { + *last_p += 1 + } ChainIndex(chain) } - pub fn n_th_son(&self, son_id: u32) -> ChainIndex { + pub fn n_th_child(&self, child_id: u32) -> ChainIndex { let mut chain = self.0.clone(); - chain.push(son_id); + chain.push(child_id); ChainIndex(chain) } } +#[derive(Debug)] +pub struct ChildKeysPublic { + pub csk: nssa::PrivateKey, + pub cpk: nssa::PublicKey, + pub ccc: [u8; 32], + ///Can be None if root + pub cci: Option, +} + +impl ChildKeysPublic { + pub fn root(seed: [u8; 64]) -> Self { + let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_pub"); + + let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); + let ccc = *hash_value.last_chunk::<32>().unwrap(); + let cpk = nssa::PublicKey::new_from_private_key(&csk); + + Self { + csk, + cpk, + ccc, + cci: None, + } + } + + pub fn n_th_child(&self, cci: u32) -> Self { + let mut hash_input = vec![]; + hash_input.extend_from_slice(self.csk.value()); + hash_input.extend_from_slice(&cci.to_le_bytes()); + + let hash_value = hmac_sha512::HMAC::mac(&hash_input, self.ccc); + + let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); + let ccc = *hash_value.last_chunk::<32>().unwrap(); + let cpk = nssa::PublicKey::new_from_private_key(&csk); + + Self { + csk, + cpk, + ccc, + cci: Some(cci), + } + } +} + #[derive(Debug)] pub struct KeyTreePublic { pub key_map: BTreeMap, @@ -89,37 +135,27 @@ pub struct KeyTreePublic { impl KeyTreePublic { pub fn new(seed: &SeedHolder) -> Self { let seed_fit: [u8; 64] = seed.seed.clone().try_into().unwrap(); - let hash_value = hmac_sha512::HMAC::mac(&seed_fit, "NSSA_master_pub"); - let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); - let ccc = *hash_value.last_chunk::<32>().unwrap(); - let cpk = nssa::PublicKey::new_from_private_key(&csk); - let address = nssa::Address::from(&cpk); - - let root_keys = ChildKeysPublic { - csk, - cpk, - ccc, - cci: None, - }; + let root_keys = ChildKeysPublic::root(seed_fit); + let address = nssa::Address::from(&root_keys.cpk); let mut key_map = BTreeMap::new(); let mut addr_map = HashMap::new(); - key_map.insert(ChainIndex::from_str("").unwrap(), root_keys); - addr_map.insert(address, ChainIndex::from_str("").unwrap()); + key_map.insert(ChainIndex::root(), root_keys); + addr_map.insert(address, ChainIndex::root()); Self { key_map, addr_map } } - pub fn find_last_son_of_id(&self, father_id: &ChainIndex) -> Option { - if !self.key_map.contains_key(father_id) { + pub fn find_next_last_child_of_id(&self, parent_id: &ChainIndex) -> Option { + if !self.key_map.contains_key(parent_id) { return None; } - let leftmost_son = father_id.n_th_son(u32::MIN); + let leftmost_child = parent_id.n_th_child(u32::MIN); - if !self.key_map.contains_key(&leftmost_son) { + if !self.key_map.contains_key(&leftmost_child) { Some(0) } else { let mut right = u32::MAX - 1; @@ -127,10 +163,10 @@ impl KeyTreePublic { let mut right_border = u32::MAX; loop { - let rightmost_son = father_id.n_th_son(right); + let rightmost_child = parent_id.n_th_child(right); - let rightmost_ref = self.key_map.get(&rightmost_son); - let rightmost_ref_next = self.key_map.get(&rightmost_son.next_in_line()); + let rightmost_ref = self.key_map.get(&rightmost_child); + let rightmost_ref_next = self.key_map.get(&rightmost_child.next_in_line()); match (&rightmost_ref, &rightmost_ref_next) { (Some(_), Some(_)) => { @@ -138,7 +174,7 @@ impl KeyTreePublic { right = (right + right_border) / 2; } (Some(_), None) => { - break Some(right); + break Some(right + 1); } (None, None) => { right_border = right; @@ -152,35 +188,21 @@ impl KeyTreePublic { } } - pub fn generate_new_pub_keys(&mut self, father_cci: ChainIndex) -> Option { - if !self.key_map.contains_key(&father_cci) { + pub fn generate_new_pub_keys(&mut self, parent_cci: ChainIndex) -> Option { + if !self.key_map.contains_key(&parent_cci) { return None; } - let father_keys = self.key_map.get(&father_cci).unwrap(); - let next_son_id = self.find_last_son_of_id(&father_cci).unwrap(); - let next_son_cci = father_cci.n_th_son(next_son_id); + let father_keys = self.key_map.get(&parent_cci).unwrap(); + let next_child_id = self.find_next_last_child_of_id(&parent_cci).unwrap(); + let next_cci = parent_cci.n_th_child(next_child_id); - let mut hash_input = vec![]; - hash_input.extend_from_slice(father_keys.csk.value()); - hash_input.extend_from_slice(&next_son_id.to_le_bytes()); + let child_keys = father_keys.n_th_child(next_child_id); - let hash_value = hmac_sha512::HMAC::mac(&hash_input, father_keys.ccc); + let address = nssa::Address::from(&child_keys.cpk); - let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); - let ccc = *hash_value.last_chunk::<32>().unwrap(); - let cpk = nssa::PublicKey::new_from_private_key(&csk); - let address = nssa::Address::from(&cpk); - - let child_keys = ChildKeysPublic { - csk, - cpk, - ccc, - cci: None, - }; - - self.key_map.insert(next_son_cci.clone(), child_keys); - self.addr_map.insert(address, next_son_cci); + self.key_map.insert(next_cci.clone(), child_keys); + self.addr_map.insert(address, next_cci); Some(address) } @@ -188,18 +210,281 @@ impl KeyTreePublic { pub fn get_pub_keys(&self, addr: nssa::Address) -> Option<&ChildKeysPublic> { self.addr_map .get(&addr) - .map(|chain_id| self.key_map.get(chain_id)) - .flatten() - } - - pub fn topology_hexdump(&self) -> String { - let mut hex_dump = String::new(); - - //Very inefficient - for chain_id in self.key_map.keys() { - hex_dump = format!("{hex_dump}{}", chain_id.to_string()); - } - - hex_dump + .and_then(|chain_id| self.key_map.get(chain_id)) + } +} + +#[cfg(test)] +mod tests { + use nssa::Address; + + use super::*; + + #[test] + fn test_chain_id_root_correct() { + let chain_id = ChainIndex::root(); + let chain_id_2 = ChainIndex::from_str("").unwrap(); + + assert_eq!(chain_id, chain_id_2); + } + + #[test] + fn test_chain_id_deser_correct() { + let chain_id = ChainIndex::from_str("01010000").unwrap(); + + assert_eq!(chain_id.chain(), &[257]); + } + + #[test] + fn test_chain_id_next_in_line_correct() { + let chain_id = ChainIndex::from_str("01010000").unwrap(); + let next_in_line = chain_id.next_in_line(); + + assert_eq!(next_in_line, ChainIndex::from_str("02010000").unwrap()); + } + + #[test] + fn test_chain_id_child_correct() { + let chain_id = ChainIndex::from_str("01010000").unwrap(); + let child = chain_id.n_th_child(3); + + assert_eq!(child, ChainIndex::from_str("0101000003000000").unwrap()); + } + + #[test] + fn test_keys_deterministic_generation() { + let root_keys = ChildKeysPublic::root([42; 64]); + let child_keys = root_keys.n_th_child(5); + + assert_eq!(root_keys.cci, None); + assert_eq!(child_keys.cci, Some(5)); + + assert_eq!( + root_keys.ccc, + [ + 61, 30, 91, 26, 133, 91, 236, 192, 231, 53, 186, 139, 11, 221, 202, 11, 178, 215, + 254, 103, 191, 60, 117, 112, 1, 226, 31, 156, 83, 104, 150, 224 + ] + ); + assert_eq!( + child_keys.ccc, + [ + 67, 26, 102, 68, 189, 155, 102, 80, 199, 188, 112, 142, 207, 157, 36, 210, 48, 224, + 35, 6, 112, 180, 11, 190, 135, 218, 9, 14, 84, 231, 58, 98 + ] + ); + + assert_eq!( + root_keys.csk.value(), + &[ + 241, 82, 246, 237, 62, 130, 116, 47, 189, 112, 99, 67, 178, 40, 115, 245, 141, 193, + 77, 164, 243, 76, 222, 64, 50, 146, 23, 145, 91, 164, 92, 116 + ] + ); + assert_eq!( + child_keys.csk.value(), + &[ + 11, 151, 27, 212, 167, 26, 77, 234, 103, 145, 53, 191, 184, 25, 240, 191, 156, 25, + 60, 144, 65, 22, 193, 163, 246, 227, 212, 81, 49, 170, 33, 158 + ] + ); + + assert_eq!( + root_keys.cpk.value(), + &[ + 220, 170, 95, 177, 121, 37, 86, 166, 56, 238, 232, 72, 21, 106, 107, 217, 158, 74, + 133, 91, 143, 244, 155, 15, 2, 230, 223, 169, 13, 20, 163, 138 + ] + ); + assert_eq!( + child_keys.cpk.value(), + &[ + 152, 249, 236, 111, 132, 96, 184, 122, 21, 179, 240, 15, 234, 155, 164, 144, 108, + 110, 120, 74, 176, 147, 196, 168, 243, 186, 203, 79, 97, 17, 194, 52 + ] + ); + } + + fn seed_holder_for_tests() -> SeedHolder { + SeedHolder { + seed: [42; 64].to_vec(), + } + } + + #[test] + fn test_simple_key_tree() { + let seed_holder = seed_holder_for_tests(); + + let tree = KeyTreePublic::new(&seed_holder); + + assert!(tree.key_map.contains_key(&ChainIndex::root())); + assert!(tree.addr_map.contains_key(&Address::new([ + 46, 223, 229, 177, 59, 18, 189, 219, 153, 31, 249, 90, 112, 230, 180, 164, 80, 25, 106, + 159, 14, 238, 1, 192, 91, 8, 210, 165, 199, 41, 60, 104, + ]))); + } + + #[test] + fn test_small_key_tree() { + let seed_holder = seed_holder_for_tests(); + + let mut tree = KeyTreePublic::new(&seed_holder); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 0); + + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + + assert!( + tree.key_map + .contains_key(&ChainIndex::from_str("00000000").unwrap()) + ); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 1); + + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 7); + } + + #[test] + fn test_key_tree_can_not_make_child_keys() { + let seed_holder = seed_holder_for_tests(); + + let mut tree = KeyTreePublic::new(&seed_holder); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 0); + + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + + assert!( + tree.key_map + .contains_key(&ChainIndex::from_str("00000000").unwrap()) + ); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 1); + + let key_opt = tree.generate_new_pub_keys(ChainIndex::from_str("03000000").unwrap()); + + assert_eq!(key_opt, None); + } + + #[test] + fn test_key_tree_complex_structure() { + let seed_holder = seed_holder_for_tests(); + + let mut tree = KeyTreePublic::new(&seed_holder); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 0); + + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + + assert!( + tree.key_map + .contains_key(&ChainIndex::from_str("00000000").unwrap()) + ); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 1); + + tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + + assert!( + tree.key_map + .contains_key(&ChainIndex::from_str("01000000").unwrap()) + ); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::root()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 2); + + tree.generate_new_pub_keys(ChainIndex::from_str("00000000").unwrap()) + .unwrap(); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::from_str("00000000").unwrap()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 1); + + assert!( + tree.key_map + .contains_key(&ChainIndex::from_str("0000000000000000").unwrap()) + ); + + tree.generate_new_pub_keys(ChainIndex::from_str("00000000").unwrap()) + .unwrap(); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::from_str("00000000").unwrap()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 2); + + assert!( + tree.key_map + .contains_key(&ChainIndex::from_str("0000000001000000").unwrap()) + ); + + tree.generate_new_pub_keys(ChainIndex::from_str("00000000").unwrap()) + .unwrap(); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::from_str("00000000").unwrap()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 3); + + assert!( + tree.key_map + .contains_key(&ChainIndex::from_str("0000000002000000").unwrap()) + ); + + tree.generate_new_pub_keys(ChainIndex::from_str("0000000001000000").unwrap()) + .unwrap(); + + assert!( + tree.key_map + .contains_key(&ChainIndex::from_str("000000000100000000000000").unwrap()) + ); + + let next_last_child_for_parent_id = tree + .find_next_last_child_of_id(&ChainIndex::from_str("0000000001000000").unwrap()) + .unwrap(); + + assert_eq!(next_last_child_for_parent_id, 1); } } From d9a130cc55eb9fe154ba434bb35c1c60fbb6b2ea Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 7 Nov 2025 16:21:14 +0200 Subject: [PATCH 04/90] feat: private tree --- .../key_management/key_tree/chain_index.rs | 115 ++++++++ .../key_management/key_tree/keys_private.rs | 201 ++++++++++++++ .../key_management/key_tree/keys_public.rs | 119 +++++++++ .../{key_tree.rs => key_tree/mod.rs} | 245 ++---------------- .../src/key_management/key_tree/traits.rs | 11 + .../src/key_management/secret_holders.rs | 4 +- key_protocol/src/key_protocol_core/mod.rs | 34 ++- nssa/src/signature/public_key.rs | 3 +- 8 files changed, 498 insertions(+), 234 deletions(-) create mode 100644 key_protocol/src/key_management/key_tree/chain_index.rs create mode 100644 key_protocol/src/key_management/key_tree/keys_private.rs create mode 100644 key_protocol/src/key_management/key_tree/keys_public.rs rename key_protocol/src/key_management/{key_tree.rs => key_tree/mod.rs} (56%) create mode 100644 key_protocol/src/key_management/key_tree/traits.rs diff --git a/key_protocol/src/key_management/key_tree/chain_index.rs b/key_protocol/src/key_management/key_tree/chain_index.rs new file mode 100644 index 0000000..dad9b2a --- /dev/null +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -0,0 +1,115 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] +pub struct ChainIndex(Vec); + +impl FromStr for ChainIndex { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Ok(Self(vec![])); + } + + let hex_decoded = hex::decode(s)?; + + if !hex_decoded.len().is_multiple_of(4) { + Err(hex::FromHexError::InvalidStringLength) + } else { + let mut res_vec = vec![]; + + for i in 0..(hex_decoded.len() / 4) { + res_vec.push(u32::from_le_bytes([ + hex_decoded[4 * i], + hex_decoded[4 * i + 1], + hex_decoded[4 * i + 2], + hex_decoded[4 * i + 3], + ])); + } + + Ok(Self(res_vec)) + } + } +} + +#[allow(clippy::to_string_trait_impl)] +impl ToString for ChainIndex { + fn to_string(&self) -> String { + if self.0.is_empty() { + return "".to_string(); + } + + let mut res_vec = vec![]; + + for index in &self.0 { + res_vec.extend_from_slice(&index.to_le_bytes()); + } + + hex::encode(res_vec) + } +} + +impl ChainIndex { + pub fn root() -> Self { + ChainIndex::from_str("").unwrap() + } + + pub fn chain(&self) -> &[u32] { + &self.0 + } + + pub fn next_in_line(&self) -> ChainIndex { + let mut chain = self.0.clone(); + //ToDo: Add overflow check + if let Some(last_p) = chain.last_mut() { + *last_p += 1 + } + + ChainIndex(chain) + } + + pub fn n_th_child(&self, child_id: u32) -> ChainIndex { + let mut chain = self.0.clone(); + chain.push(child_id); + + ChainIndex(chain) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_chain_id_root_correct() { + let chain_id = ChainIndex::root(); + let chain_id_2 = ChainIndex::from_str("").unwrap(); + + assert_eq!(chain_id, chain_id_2); + } + + #[test] + fn test_chain_id_deser_correct() { + let chain_id = ChainIndex::from_str("01010000").unwrap(); + + assert_eq!(chain_id.chain(), &[257]); + } + + #[test] + fn test_chain_id_next_in_line_correct() { + let chain_id = ChainIndex::from_str("01010000").unwrap(); + let next_in_line = chain_id.next_in_line(); + + assert_eq!(next_in_line, ChainIndex::from_str("02010000").unwrap()); + } + + #[test] + fn test_chain_id_child_correct() { + let chain_id = ChainIndex::from_str("01010000").unwrap(); + let child = chain_id.n_th_child(3); + + assert_eq!(child, ChainIndex::from_str("0101000003000000").unwrap()); + } +} diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs new file mode 100644 index 0000000..46ade9f --- /dev/null +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -0,0 +1,201 @@ +use k256::{Scalar, elliptic_curve::PrimeField}; +use nssa_core::{NullifierPublicKey, NullifierSecretKey, encryption::IncomingViewingPublicKey}; +use serde::{Deserialize, Serialize}; + +use crate::key_management::{ + key_tree::traits::KeyNode, + secret_holders::{IncomingViewingSecretKey, OutgoingViewingSecretKey, SecretSpendingKey}, +}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ChildKeysPrivate { + pub ssk: SecretSpendingKey, + pub nsk: NullifierSecretKey, + pub isk: IncomingViewingSecretKey, + pub ovk: OutgoingViewingSecretKey, + pub npk: NullifierPublicKey, + pub ipk: IncomingViewingPublicKey, + pub ccc: [u8; 32], + ///Can be None if root + pub cci: Option, + pub account: nssa::Account, +} + +impl KeyNode for ChildKeysPrivate { + fn root(seed: [u8; 64]) -> Self { + let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_priv"); + + let ssk = SecretSpendingKey(*hash_value.first_chunk::<32>().unwrap()); + let ccc = *hash_value.last_chunk::<32>().unwrap(); + + let nsk = ssk.generate_nullifier_secret_key(); + let isk = ssk.generate_incoming_viewing_secret_key(); + let ovk = ssk.generate_outgoing_viewing_secret_key(); + + let npk = (&nsk).into(); + let ipk = IncomingViewingPublicKey::from_scalar(isk); + + Self { + ssk, + nsk, + isk, + ovk, + npk, + ipk, + ccc, + cci: None, + account: nssa::Account::default(), + } + } + + fn n_th_child(&self, cci: u32) -> Self { + let parent_pt = Scalar::from_repr(self.ovk.into()).unwrap() + + Scalar::from_repr(self.nsk.into()).unwrap() + * Scalar::from_repr(self.isk.into()).unwrap(); + let mut input = vec![]; + + input.extend_from_slice(b"NSSA_seed_priv"); + input.extend_from_slice(&parent_pt.to_bytes()); + input.extend_from_slice(&cci.to_le_bytes()); + + let hash_value = hmac_sha512::HMAC::mac(input, self.ccc); + + let ssk = SecretSpendingKey(*hash_value.first_chunk::<32>().unwrap()); + let ccc = *hash_value.last_chunk::<32>().unwrap(); + + let nsk = ssk.generate_nullifier_secret_key(); + let isk = ssk.generate_incoming_viewing_secret_key(); + let ovk = ssk.generate_outgoing_viewing_secret_key(); + + let npk = (&nsk).into(); + let ipk = IncomingViewingPublicKey::from_scalar(isk); + + Self { + ssk, + nsk, + isk, + ovk, + npk, + ipk, + ccc, + cci: Some(cci), + account: nssa::Account::default(), + } + } + + fn chain_code(&self) -> &[u8; 32] { + &self.ccc + } + + fn child_index(&self) -> &Option { + &self.cci + } + + fn address(&self) -> nssa::Address { + nssa::Address::from(&self.npk) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keys_deterministic_generation() { + let root_keys = ChildKeysPrivate::root([42; 64]); + let child_keys = root_keys.n_th_child(5); + + assert_eq!(root_keys.cci, None); + assert_eq!(child_keys.cci, Some(5)); + + assert_eq!( + root_keys.ssk.0, + [ + 249, 83, 253, 32, 174, 204, 185, 44, 253, 167, 61, 92, 128, 5, 152, 4, 220, 21, 88, + 84, 167, 180, 154, 249, 44, 77, 33, 136, 59, 131, 203, 152 + ] + ); + assert_eq!( + child_keys.ssk.0, + [ + 16, 242, 229, 242, 252, 158, 153, 210, 234, 120, 70, 85, 83, 196, 5, 53, 28, 26, + 187, 230, 22, 193, 146, 232, 237, 3, 166, 184, 122, 1, 233, 93 + ] + ); + + assert_eq!( + root_keys.nsk, + [ + 38, 195, 52, 182, 16, 66, 167, 156, 9, 14, 65, 100, 17, 93, 166, 71, 27, 148, 93, + 85, 116, 109, 130, 8, 195, 222, 159, 214, 141, 41, 124, 57 + ] + ); + assert_eq!( + child_keys.nsk, + [ + 215, 46, 2, 151, 174, 60, 86, 154, 5, 3, 175, 245, 12, 176, 220, 58, 250, 118, 236, + 49, 254, 221, 229, 58, 40, 1, 170, 145, 175, 108, 23, 170 + ] + ); + + assert_eq!( + root_keys.isk, + [ + 153, 161, 15, 34, 96, 184, 165, 165, 27, 244, 155, 40, 70, 5, 241, 133, 78, 40, 61, + 118, 48, 148, 226, 5, 97, 18, 201, 128, 82, 248, 163, 72 + ] + ); + assert_eq!( + child_keys.isk, + [ + 192, 155, 55, 43, 164, 115, 71, 145, 227, 225, 21, 57, 55, 12, 226, 44, 10, 103, + 39, 73, 230, 173, 60, 69, 69, 122, 110, 241, 164, 3, 192, 57 + ] + ); + + assert_eq!( + root_keys.ovk, + [ + 205, 87, 71, 129, 90, 242, 217, 200, 140, 252, 124, 46, 207, 7, 33, 156, 83, 166, + 150, 81, 98, 131, 182, 156, 110, 92, 78, 140, 125, 218, 152, 154 + ] + ); + assert_eq!( + child_keys.ovk, + [ + 131, 202, 219, 172, 219, 29, 48, 120, 226, 209, 209, 10, 216, 173, 48, 167, 233, + 17, 35, 155, 30, 217, 176, 120, 72, 146, 250, 226, 165, 178, 255, 90 + ] + ); + + assert_eq!( + root_keys.npk.0, + [ + 65, 176, 149, 243, 192, 45, 216, 177, 169, 56, 229, 7, 28, 66, 204, 87, 109, 83, + 152, 64, 14, 188, 179, 210, 147, 60, 22, 251, 203, 70, 89, 215 + ] + ); + assert_eq!( + child_keys.npk.0, + [ + 69, 104, 130, 115, 48, 134, 19, 188, 67, 148, 163, 54, 155, 237, 57, 27, 136, 228, + 111, 233, 205, 158, 149, 31, 84, 11, 241, 176, 243, 12, 138, 249 + ] + ); + + assert_eq!( + root_keys.ipk.0, + &[ + 3, 174, 56, 136, 244, 179, 18, 122, 38, 220, 36, 50, 200, 41, 104, 167, 70, 18, 60, + 202, 93, 193, 29, 16, 125, 252, 96, 51, 199, 152, 47, 233, 178 + ] + ); + assert_eq!( + child_keys.ipk.0, + &[ + 3, 18, 202, 246, 79, 141, 169, 51, 55, 202, 120, 169, 244, 201, 156, 162, 216, 115, + 126, 53, 46, 94, 235, 125, 114, 178, 215, 81, 171, 93, 93, 88, 117 + ] + ); + } +} diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs new file mode 100644 index 0000000..4e1bf1e --- /dev/null +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -0,0 +1,119 @@ +use serde::{Deserialize, Serialize}; + +use crate::key_management::key_tree::traits::KeyNode; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ChildKeysPublic { + pub csk: nssa::PrivateKey, + pub cpk: nssa::PublicKey, + pub ccc: [u8; 32], + ///Can be None if root + pub cci: Option, +} + +impl KeyNode for ChildKeysPublic { + fn root(seed: [u8; 64]) -> Self { + let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_pub"); + + let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); + let ccc = *hash_value.last_chunk::<32>().unwrap(); + let cpk = nssa::PublicKey::new_from_private_key(&csk); + + Self { + csk, + cpk, + ccc, + cci: None, + } + } + + fn n_th_child(&self, cci: u32) -> Self { + let mut hash_input = vec![]; + hash_input.extend_from_slice(self.csk.value()); + hash_input.extend_from_slice(&cci.to_le_bytes()); + + let hash_value = hmac_sha512::HMAC::mac(&hash_input, self.ccc); + + let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); + let ccc = *hash_value.last_chunk::<32>().unwrap(); + let cpk = nssa::PublicKey::new_from_private_key(&csk); + + Self { + csk, + cpk, + ccc, + cci: Some(cci), + } + } + + fn chain_code(&self) -> &[u8; 32] { + &self.ccc + } + + fn child_index(&self) -> &Option { + &self.cci + } + + fn address(&self) -> nssa::Address { + nssa::Address::from(&self.cpk) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keys_deterministic_generation() { + let root_keys = ChildKeysPublic::root([42; 64]); + let child_keys = root_keys.n_th_child(5); + + assert_eq!(root_keys.cci, None); + assert_eq!(child_keys.cci, Some(5)); + + assert_eq!( + root_keys.ccc, + [ + 61, 30, 91, 26, 133, 91, 236, 192, 231, 53, 186, 139, 11, 221, 202, 11, 178, 215, + 254, 103, 191, 60, 117, 112, 1, 226, 31, 156, 83, 104, 150, 224 + ] + ); + assert_eq!( + child_keys.ccc, + [ + 67, 26, 102, 68, 189, 155, 102, 80, 199, 188, 112, 142, 207, 157, 36, 210, 48, 224, + 35, 6, 112, 180, 11, 190, 135, 218, 9, 14, 84, 231, 58, 98 + ] + ); + + assert_eq!( + root_keys.csk.value(), + &[ + 241, 82, 246, 237, 62, 130, 116, 47, 189, 112, 99, 67, 178, 40, 115, 245, 141, 193, + 77, 164, 243, 76, 222, 64, 50, 146, 23, 145, 91, 164, 92, 116 + ] + ); + assert_eq!( + child_keys.csk.value(), + &[ + 11, 151, 27, 212, 167, 26, 77, 234, 103, 145, 53, 191, 184, 25, 240, 191, 156, 25, + 60, 144, 65, 22, 193, 163, 246, 227, 212, 81, 49, 170, 33, 158 + ] + ); + + assert_eq!( + root_keys.cpk.value(), + &[ + 220, 170, 95, 177, 121, 37, 86, 166, 56, 238, 232, 72, 21, 106, 107, 217, 158, 74, + 133, 91, 143, 244, 155, 15, 2, 230, 223, 169, 13, 20, 163, 138 + ] + ); + assert_eq!( + child_keys.cpk.value(), + &[ + 152, 249, 236, 111, 132, 96, 184, 122, 21, 179, 240, 15, 234, 155, 164, 144, 108, + 110, 120, 74, 176, 147, 196, 168, 243, 186, 203, 79, 97, 17, 194, 52 + ] + ); + } +} diff --git a/key_protocol/src/key_management/key_tree.rs b/key_protocol/src/key_management/key_tree/mod.rs similarity index 56% rename from key_protocol/src/key_management/key_tree.rs rename to key_protocol/src/key_management/key_tree/mod.rs index dd59363..c21cc6e 100644 --- a/key_protocol/src/key_management/key_tree.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -1,143 +1,35 @@ -use std::{ - collections::{BTreeMap, HashMap}, - str::FromStr, +use std::collections::{BTreeMap, HashMap}; + +use serde::{Deserialize, Serialize}; + +use crate::key_management::{ + key_tree::{ + chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, + traits::KeyNode, + }, + secret_holders::SeedHolder, }; -use crate::key_management::secret_holders::SeedHolder; +pub mod chain_index; +pub mod keys_private; +pub mod keys_public; +pub mod traits; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] -pub struct ChainIndex(Vec); - -impl FromStr for ChainIndex { - type Err = hex::FromHexError; - - fn from_str(s: &str) -> Result { - if s.is_empty() { - return Ok(Self(vec![])); - } - - let hex_decoded = hex::decode(s)?; - - if !hex_decoded.len().is_multiple_of(4) { - Err(hex::FromHexError::InvalidStringLength) - } else { - let mut res_vec = vec![]; - - for i in 0..(hex_decoded.len() / 4) { - res_vec.push(u32::from_le_bytes([ - hex_decoded[4 * i], - hex_decoded[4 * i + 1], - hex_decoded[4 * i + 2], - hex_decoded[4 * i + 3], - ])); - } - - Ok(Self(res_vec)) - } - } -} - -#[allow(clippy::to_string_trait_impl)] -impl ToString for ChainIndex { - fn to_string(&self) -> String { - if self.0.is_empty() { - return "".to_string(); - } - - let mut res_vec = vec![]; - - for index in &self.0 { - res_vec.extend_from_slice(&index.to_le_bytes()); - } - - hex::encode(res_vec) - } -} - -impl ChainIndex { - pub fn root() -> Self { - ChainIndex::from_str("").unwrap() - } - - pub fn chain(&self) -> &[u32] { - &self.0 - } - - pub fn next_in_line(&self) -> ChainIndex { - let mut chain = self.0.clone(); - //ToDo: Add overflow check - if let Some(last_p) = chain.last_mut() { - *last_p += 1 - } - - ChainIndex(chain) - } - - pub fn n_th_child(&self, child_id: u32) -> ChainIndex { - let mut chain = self.0.clone(); - chain.push(child_id); - - ChainIndex(chain) - } -} - -#[derive(Debug)] -pub struct ChildKeysPublic { - pub csk: nssa::PrivateKey, - pub cpk: nssa::PublicKey, - pub ccc: [u8; 32], - ///Can be None if root - pub cci: Option, -} - -impl ChildKeysPublic { - pub fn root(seed: [u8; 64]) -> Self { - let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_pub"); - - let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); - let ccc = *hash_value.last_chunk::<32>().unwrap(); - let cpk = nssa::PublicKey::new_from_private_key(&csk); - - Self { - csk, - cpk, - ccc, - cci: None, - } - } - - pub fn n_th_child(&self, cci: u32) -> Self { - let mut hash_input = vec![]; - hash_input.extend_from_slice(self.csk.value()); - hash_input.extend_from_slice(&cci.to_le_bytes()); - - let hash_value = hmac_sha512::HMAC::mac(&hash_input, self.ccc); - - let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); - let ccc = *hash_value.last_chunk::<32>().unwrap(); - let cpk = nssa::PublicKey::new_from_private_key(&csk); - - Self { - csk, - cpk, - ccc, - cci: Some(cci), - } - } -} - -#[derive(Debug)] -pub struct KeyTreePublic { - pub key_map: BTreeMap, +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct KeyTree { + pub key_map: BTreeMap, pub addr_map: HashMap, } -impl KeyTreePublic { +pub type KeyTreePublic = KeyTree; +pub type KeyTreePrivate = KeyTree; + +impl KeyTree { pub fn new(seed: &SeedHolder) -> Self { let seed_fit: [u8; 64] = seed.seed.clone().try_into().unwrap(); - let root_keys = ChildKeysPublic::root(seed_fit); - let address = nssa::Address::from(&root_keys.cpk); + let root_keys = Node::root(seed_fit); + let address = root_keys.address(); let mut key_map = BTreeMap::new(); let mut addr_map = HashMap::new(); @@ -199,7 +91,7 @@ impl KeyTreePublic { let child_keys = father_keys.n_th_child(next_child_id); - let address = nssa::Address::from(&child_keys.cpk); + let address = child_keys.address(); self.key_map.insert(next_cci.clone(), child_keys); self.addr_map.insert(address, next_cci); @@ -207,7 +99,7 @@ impl KeyTreePublic { Some(address) } - pub fn get_pub_keys(&self, addr: nssa::Address) -> Option<&ChildKeysPublic> { + pub fn get_pub_keys(&self, addr: nssa::Address) -> Option<&Node> { self.addr_map .get(&addr) .and_then(|chain_id| self.key_map.get(chain_id)) @@ -216,95 +108,12 @@ impl KeyTreePublic { #[cfg(test)] mod tests { + use std::str::FromStr; + use nssa::Address; use super::*; - #[test] - fn test_chain_id_root_correct() { - let chain_id = ChainIndex::root(); - let chain_id_2 = ChainIndex::from_str("").unwrap(); - - assert_eq!(chain_id, chain_id_2); - } - - #[test] - fn test_chain_id_deser_correct() { - let chain_id = ChainIndex::from_str("01010000").unwrap(); - - assert_eq!(chain_id.chain(), &[257]); - } - - #[test] - fn test_chain_id_next_in_line_correct() { - let chain_id = ChainIndex::from_str("01010000").unwrap(); - let next_in_line = chain_id.next_in_line(); - - assert_eq!(next_in_line, ChainIndex::from_str("02010000").unwrap()); - } - - #[test] - fn test_chain_id_child_correct() { - let chain_id = ChainIndex::from_str("01010000").unwrap(); - let child = chain_id.n_th_child(3); - - assert_eq!(child, ChainIndex::from_str("0101000003000000").unwrap()); - } - - #[test] - fn test_keys_deterministic_generation() { - let root_keys = ChildKeysPublic::root([42; 64]); - let child_keys = root_keys.n_th_child(5); - - assert_eq!(root_keys.cci, None); - assert_eq!(child_keys.cci, Some(5)); - - assert_eq!( - root_keys.ccc, - [ - 61, 30, 91, 26, 133, 91, 236, 192, 231, 53, 186, 139, 11, 221, 202, 11, 178, 215, - 254, 103, 191, 60, 117, 112, 1, 226, 31, 156, 83, 104, 150, 224 - ] - ); - assert_eq!( - child_keys.ccc, - [ - 67, 26, 102, 68, 189, 155, 102, 80, 199, 188, 112, 142, 207, 157, 36, 210, 48, 224, - 35, 6, 112, 180, 11, 190, 135, 218, 9, 14, 84, 231, 58, 98 - ] - ); - - assert_eq!( - root_keys.csk.value(), - &[ - 241, 82, 246, 237, 62, 130, 116, 47, 189, 112, 99, 67, 178, 40, 115, 245, 141, 193, - 77, 164, 243, 76, 222, 64, 50, 146, 23, 145, 91, 164, 92, 116 - ] - ); - assert_eq!( - child_keys.csk.value(), - &[ - 11, 151, 27, 212, 167, 26, 77, 234, 103, 145, 53, 191, 184, 25, 240, 191, 156, 25, - 60, 144, 65, 22, 193, 163, 246, 227, 212, 81, 49, 170, 33, 158 - ] - ); - - assert_eq!( - root_keys.cpk.value(), - &[ - 220, 170, 95, 177, 121, 37, 86, 166, 56, 238, 232, 72, 21, 106, 107, 217, 158, 74, - 133, 91, 143, 244, 155, 15, 2, 230, 223, 169, 13, 20, 163, 138 - ] - ); - assert_eq!( - child_keys.cpk.value(), - &[ - 152, 249, 236, 111, 132, 96, 184, 122, 21, 179, 240, 15, 234, 155, 164, 144, 108, - 110, 120, 74, 176, 147, 196, 168, 243, 186, 203, 79, 97, 17, 194, 52 - ] - ); - } - fn seed_holder_for_tests() -> SeedHolder { SeedHolder { seed: [42; 64].to_vec(), diff --git a/key_protocol/src/key_management/key_tree/traits.rs b/key_protocol/src/key_management/key_tree/traits.rs new file mode 100644 index 0000000..662481a --- /dev/null +++ b/key_protocol/src/key_management/key_tree/traits.rs @@ -0,0 +1,11 @@ +pub trait KeyNode { + fn root(seed: [u8; 64]) -> Self; + + fn n_th_child(&self, cci: u32) -> Self; + + fn chain_code(&self) -> &[u8; 32]; + + fn child_index(&self) -> &Option; + + fn address(&self) -> nssa::Address; +} diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index f05a641..ee7aced 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -45,8 +45,8 @@ impl SeedHolder { } pub fn new_mnemonic(passphrase: String) -> Self { - let mut enthopy_bytes: [u8; 32] = [0; 32]; - OsRng.fill_bytes(&mut enthopy_bytes); + //Enthropy bytes must be deterministic as well + let enthopy_bytes: [u8; 32] = [0; 32]; let mnemonic = Mnemonic::from_entropy(&enthopy_bytes).unwrap(); let seed_wide = mnemonic.to_seed(passphrase); diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 33a007a..6731da8 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -4,18 +4,26 @@ use anyhow::Result; use k256::AffinePoint; use serde::{Deserialize, Serialize}; -use crate::key_management::KeyChain; +use crate::key_management::{ + KeyChain, + key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, +}; pub type PublicKey = AffinePoint; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NSSAUserData { - ///Map for all user public accounts - pub pub_account_signing_keys: HashMap, - ///Map for all user private accounts - pub user_private_accounts: HashMap, + ///Default public accounts + pub default_pub_account_signing_keys: HashMap, + ///Default private accounts + pub default_user_private_accounts: + HashMap, ///Mnemonic passphrase pub password: String, + /// Tree of public keys + pub public_key_tree: KeyTreePublic, + /// Tree of private keys + pub private_key_tree: KeyTreePrivate, } impl NSSAUserData { @@ -97,13 +105,13 @@ impl NSSAUserData { /// Generated new private key for public transaction signatures /// /// Returns the address of new account - pub fn generate_new_public_transaction_private_key(&mut self) -> nssa::Address { - let private_key = nssa::PrivateKey::new_os_random(); - let address = nssa::Address::from(&nssa::PublicKey::new_from_private_key(&private_key)); - - self.pub_account_signing_keys.insert(address, private_key); - - address + pub fn generate_new_public_transaction_private_key( + &mut self, + parent_cci: ChainIndex, + ) -> nssa::Address { + self.public_key_tree + .generate_new_pub_keys(parent_cci) + .unwrap() } /// Returns the signing key for public transaction signatures @@ -111,7 +119,7 @@ impl NSSAUserData { &self, address: &nssa::Address, ) -> Option<&nssa::PrivateKey> { - self.pub_account_signing_keys.get(address) + self.public_key_tree.get_pub_keys(address) } /// Generated new private key for privacy preserving transactions diff --git a/nssa/src/signature/public_key.rs b/nssa/src/signature/public_key.rs index dbd7d64..095025d 100644 --- a/nssa/src/signature/public_key.rs +++ b/nssa/src/signature/public_key.rs @@ -1,10 +1,11 @@ use nssa_core::address::Address; +use serde::{Deserialize, Serialize}; use crate::{PrivateKey, error::NssaError}; use sha2::{Digest, Sha256}; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PublicKey([u8; 32]); impl PublicKey { From 20c276e63eb0c75a553bf5b3454fa6e67732d981 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Mon, 10 Nov 2025 16:29:33 +0200 Subject: [PATCH 05/90] fix: continuation of integration --- .../key_management/key_tree/keys_private.rs | 128 ++++++++++++------ .../key_management/key_tree/keys_public.rs | 6 + .../src/key_management/key_tree/mod.rs | 45 +++--- key_protocol/src/key_management/mod.rs | 2 +- key_protocol/src/key_protocol_core/mod.rs | 111 +++++++++------ wallet/src/chain_storage/mod.rs | 69 ++++++---- wallet/src/cli/account.rs | 22 ++- wallet/src/config.rs | 15 +- wallet/src/helperfunctions.rs | 31 +++-- wallet/src/lib.rs | 22 ++- 10 files changed, 290 insertions(+), 161 deletions(-) diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index 46ade9f..84b44fa 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -1,24 +1,19 @@ use k256::{Scalar, elliptic_curve::PrimeField}; -use nssa_core::{NullifierPublicKey, NullifierSecretKey, encryption::IncomingViewingPublicKey}; +use nssa_core::encryption::IncomingViewingPublicKey; use serde::{Deserialize, Serialize}; use crate::key_management::{ + KeyChain, key_tree::traits::KeyNode, - secret_holders::{IncomingViewingSecretKey, OutgoingViewingSecretKey, SecretSpendingKey}, + secret_holders::{PrivateKeyHolder, SecretSpendingKey}, }; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChildKeysPrivate { - pub ssk: SecretSpendingKey, - pub nsk: NullifierSecretKey, - pub isk: IncomingViewingSecretKey, - pub ovk: OutgoingViewingSecretKey, - pub npk: NullifierPublicKey, - pub ipk: IncomingViewingPublicKey, + pub value: (KeyChain, nssa::Account), pub ccc: [u8; 32], ///Can be None if root pub cci: Option, - pub account: nssa::Account, } impl KeyNode for ChildKeysPrivate { @@ -36,22 +31,43 @@ impl KeyNode for ChildKeysPrivate { let ipk = IncomingViewingPublicKey::from_scalar(isk); Self { - ssk, - nsk, - isk, - ovk, - npk, - ipk, + value: ( + KeyChain { + secret_spending_key: ssk, + nullifer_public_key: npk, + incoming_viewing_public_key: ipk, + private_key_holder: PrivateKeyHolder { + nullifier_secret_key: nsk, + incoming_viewing_secret_key: isk, + outgoing_viewing_secret_key: ovk, + }, + }, + nssa::Account::default(), + ), ccc, cci: None, - account: nssa::Account::default(), } } fn n_th_child(&self, cci: u32) -> Self { - let parent_pt = Scalar::from_repr(self.ovk.into()).unwrap() - + Scalar::from_repr(self.nsk.into()).unwrap() - * Scalar::from_repr(self.isk.into()).unwrap(); + let parent_pt = Scalar::from_repr( + self.value + .0 + .private_key_holder + .outgoing_viewing_secret_key + .into(), + ) + .unwrap() + + Scalar::from_repr(self.value.0.private_key_holder.nullifier_secret_key.into()) + .unwrap() + * Scalar::from_repr( + self.value + .0 + .private_key_holder + .incoming_viewing_secret_key + .into(), + ) + .unwrap(); let mut input = vec![]; input.extend_from_slice(b"NSSA_seed_priv"); @@ -71,15 +87,21 @@ impl KeyNode for ChildKeysPrivate { let ipk = IncomingViewingPublicKey::from_scalar(isk); Self { - ssk, - nsk, - isk, - ovk, - npk, - ipk, + value: ( + KeyChain { + secret_spending_key: ssk, + nullifer_public_key: npk, + incoming_viewing_public_key: ipk, + private_key_holder: PrivateKeyHolder { + nullifier_secret_key: nsk, + incoming_viewing_secret_key: isk, + outgoing_viewing_secret_key: ovk, + }, + }, + nssa::Account::default(), + ), ccc, cci: Some(cci), - account: nssa::Account::default(), } } @@ -92,7 +114,19 @@ impl KeyNode for ChildKeysPrivate { } fn address(&self) -> nssa::Address { - nssa::Address::from(&self.npk) + nssa::Address::from(&self.value.0.nullifer_public_key) + } +} + +impl<'a> From<&'a ChildKeysPrivate> for &'a (KeyChain, nssa::Account) { + fn from(value: &'a ChildKeysPrivate) -> Self { + &value.value + } +} + +impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) { + fn from(value: &'a mut ChildKeysPrivate) -> Self { + &mut value.value } } @@ -109,14 +143,14 @@ mod tests { assert_eq!(child_keys.cci, Some(5)); assert_eq!( - root_keys.ssk.0, + root_keys.value.0.secret_spending_key.0, [ 249, 83, 253, 32, 174, 204, 185, 44, 253, 167, 61, 92, 128, 5, 152, 4, 220, 21, 88, 84, 167, 180, 154, 249, 44, 77, 33, 136, 59, 131, 203, 152 ] ); assert_eq!( - child_keys.ssk.0, + child_keys.value.0.secret_spending_key.0, [ 16, 242, 229, 242, 252, 158, 153, 210, 234, 120, 70, 85, 83, 196, 5, 53, 28, 26, 187, 230, 22, 193, 146, 232, 237, 3, 166, 184, 122, 1, 233, 93 @@ -124,14 +158,14 @@ mod tests { ); assert_eq!( - root_keys.nsk, + root_keys.value.0.private_key_holder.nullifier_secret_key, [ 38, 195, 52, 182, 16, 66, 167, 156, 9, 14, 65, 100, 17, 93, 166, 71, 27, 148, 93, 85, 116, 109, 130, 8, 195, 222, 159, 214, 141, 41, 124, 57 ] ); assert_eq!( - child_keys.nsk, + child_keys.value.0.private_key_holder.nullifier_secret_key, [ 215, 46, 2, 151, 174, 60, 86, 154, 5, 3, 175, 245, 12, 176, 220, 58, 250, 118, 236, 49, 254, 221, 229, 58, 40, 1, 170, 145, 175, 108, 23, 170 @@ -139,14 +173,22 @@ mod tests { ); assert_eq!( - root_keys.isk, + root_keys + .value + .0 + .private_key_holder + .incoming_viewing_secret_key, [ 153, 161, 15, 34, 96, 184, 165, 165, 27, 244, 155, 40, 70, 5, 241, 133, 78, 40, 61, 118, 48, 148, 226, 5, 97, 18, 201, 128, 82, 248, 163, 72 ] ); assert_eq!( - child_keys.isk, + child_keys + .value + .0 + .private_key_holder + .incoming_viewing_secret_key, [ 192, 155, 55, 43, 164, 115, 71, 145, 227, 225, 21, 57, 55, 12, 226, 44, 10, 103, 39, 73, 230, 173, 60, 69, 69, 122, 110, 241, 164, 3, 192, 57 @@ -154,14 +196,22 @@ mod tests { ); assert_eq!( - root_keys.ovk, + root_keys + .value + .0 + .private_key_holder + .outgoing_viewing_secret_key, [ 205, 87, 71, 129, 90, 242, 217, 200, 140, 252, 124, 46, 207, 7, 33, 156, 83, 166, 150, 81, 98, 131, 182, 156, 110, 92, 78, 140, 125, 218, 152, 154 ] ); assert_eq!( - child_keys.ovk, + child_keys + .value + .0 + .private_key_holder + .outgoing_viewing_secret_key, [ 131, 202, 219, 172, 219, 29, 48, 120, 226, 209, 209, 10, 216, 173, 48, 167, 233, 17, 35, 155, 30, 217, 176, 120, 72, 146, 250, 226, 165, 178, 255, 90 @@ -169,14 +219,14 @@ mod tests { ); assert_eq!( - root_keys.npk.0, + root_keys.value.0.nullifer_public_key.0, [ 65, 176, 149, 243, 192, 45, 216, 177, 169, 56, 229, 7, 28, 66, 204, 87, 109, 83, 152, 64, 14, 188, 179, 210, 147, 60, 22, 251, 203, 70, 89, 215 ] ); assert_eq!( - child_keys.npk.0, + child_keys.value.0.nullifer_public_key.0, [ 69, 104, 130, 115, 48, 134, 19, 188, 67, 148, 163, 54, 155, 237, 57, 27, 136, 228, 111, 233, 205, 158, 149, 31, 84, 11, 241, 176, 243, 12, 138, 249 @@ -184,14 +234,14 @@ mod tests { ); assert_eq!( - root_keys.ipk.0, + root_keys.value.0.incoming_viewing_public_key.0, &[ 3, 174, 56, 136, 244, 179, 18, 122, 38, 220, 36, 50, 200, 41, 104, 167, 70, 18, 60, 202, 93, 193, 29, 16, 125, 252, 96, 51, 199, 152, 47, 233, 178 ] ); assert_eq!( - child_keys.ipk.0, + child_keys.value.0.incoming_viewing_public_key.0, &[ 3, 18, 202, 246, 79, 141, 169, 51, 55, 202, 120, 169, 244, 201, 156, 162, 216, 115, 126, 53, 46, 94, 235, 125, 114, 178, 215, 81, 171, 93, 93, 88, 117 diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs index 4e1bf1e..7ca6247 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -59,6 +59,12 @@ impl KeyNode for ChildKeysPublic { } } +impl<'a> From<&'a ChildKeysPublic> for &'a nssa::PrivateKey { + fn from(value: &'a ChildKeysPublic) -> Self { + &value.csk + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index c21cc6e..7ea2a2a 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -80,7 +80,7 @@ impl KeyTree { } } - pub fn generate_new_pub_keys(&mut self, parent_cci: ChainIndex) -> Option { + pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option { if !self.key_map.contains_key(&parent_cci) { return None; } @@ -99,11 +99,22 @@ impl KeyTree { Some(address) } - pub fn get_pub_keys(&self, addr: nssa::Address) -> Option<&Node> { + pub fn get_node(&self, addr: nssa::Address) -> Option<&Node> { self.addr_map .get(&addr) .and_then(|chain_id| self.key_map.get(chain_id)) } + + pub fn get_node_mut(&mut self, addr: nssa::Address) -> Option<&mut Node> { + self.addr_map + .get(&addr) + .and_then(|chain_id| self.key_map.get_mut(chain_id)) + } + + pub fn insert(&mut self, addr: nssa::Address, chain_index: ChainIndex, node: Node) { + self.addr_map.insert(addr, chain_index.clone()); + self.key_map.insert(chain_index, node); + } } #[cfg(test)] @@ -145,7 +156,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_node(ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -158,12 +169,12 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); - tree.generate_new_pub_keys(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()) @@ -184,7 +195,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_node(ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -197,7 +208,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - let key_opt = tree.generate_new_pub_keys(ChainIndex::from_str("03000000").unwrap()); + let key_opt = tree.generate_new_node(ChainIndex::from_str("03000000").unwrap()); assert_eq!(key_opt, None); } @@ -214,7 +225,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 0); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_node(ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -227,7 +238,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - tree.generate_new_pub_keys(ChainIndex::root()).unwrap(); + tree.generate_new_node(ChainIndex::root()).unwrap(); assert!( tree.key_map @@ -240,7 +251,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 2); - tree.generate_new_pub_keys(ChainIndex::from_str("00000000").unwrap()) + tree.generate_new_node(ChainIndex::from_str("00000000").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -254,7 +265,7 @@ mod tests { .contains_key(&ChainIndex::from_str("0000000000000000").unwrap()) ); - tree.generate_new_pub_keys(ChainIndex::from_str("00000000").unwrap()) + tree.generate_new_node(ChainIndex::from_str("00000000").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -268,7 +279,7 @@ mod tests { .contains_key(&ChainIndex::from_str("0000000001000000").unwrap()) ); - tree.generate_new_pub_keys(ChainIndex::from_str("00000000").unwrap()) + tree.generate_new_node(ChainIndex::from_str("00000000").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree @@ -282,7 +293,7 @@ mod tests { .contains_key(&ChainIndex::from_str("0000000002000000").unwrap()) ); - tree.generate_new_pub_keys(ChainIndex::from_str("0000000001000000").unwrap()) + tree.generate_new_node(ChainIndex::from_str("0000000001000000").unwrap()) .unwrap(); assert!( diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index 4c3d5e0..8a58d4a 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -14,7 +14,7 @@ pub mod secret_holders; #[derive(Serialize, Deserialize, Clone, Debug)] ///Entrypoint to key management pub struct KeyChain { - secret_spending_key: SecretSpendingKey, + pub secret_spending_key: SecretSpendingKey, pub private_key_holder: PrivateKeyHolder, pub nullifer_public_key: NullifierPublicKey, pub incoming_viewing_public_key: IncomingViewingPublicKey, diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 6731da8..f50088a 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::key_management::{ KeyChain, key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, + secret_holders::SeedHolder, }; pub type PublicKey = AffinePoint; @@ -56,48 +57,62 @@ impl NSSAUserData { } pub fn new_with_accounts( - accounts_keys: HashMap, - accounts_key_chains: HashMap, + default_accounts_keys: HashMap, + default_accounts_key_chains: HashMap< + nssa::Address, + (KeyChain, nssa_core::account::Account), + >, + public_key_tree: KeyTreePublic, + private_key_tree: KeyTreePrivate, ) -> Result { - if !Self::valid_public_key_transaction_pairing_check(&accounts_keys) { + if !Self::valid_public_key_transaction_pairing_check(&default_accounts_keys) { anyhow::bail!( "Key transaction pairing check not satisfied, there is addresses, which is not derived from keys" ); } - if !Self::valid_private_key_transaction_pairing_check(&accounts_key_chains) { + if !Self::valid_private_key_transaction_pairing_check(&default_accounts_key_chains) { anyhow::bail!( "Key transaction pairing check not satisfied, there is addresses, which is not derived from keys" ); } Ok(Self { - pub_account_signing_keys: accounts_keys, - user_private_accounts: accounts_key_chains, + default_pub_account_signing_keys: default_accounts_keys, + default_user_private_accounts: default_accounts_key_chains, + public_key_tree, + private_key_tree, password: "mnemonic".to_string(), }) } pub fn new_with_accounts_and_password( - accounts_keys: HashMap, - accounts_key_chains: HashMap, + default_accounts_keys: HashMap, + default_accounts_key_chains: HashMap< + nssa::Address, + (KeyChain, nssa_core::account::Account), + >, + public_key_tree: KeyTreePublic, + private_key_tree: KeyTreePrivate, password: String, ) -> Result { - if !Self::valid_public_key_transaction_pairing_check(&accounts_keys) { + if !Self::valid_public_key_transaction_pairing_check(&default_accounts_keys) { anyhow::bail!( "Key transaction pairing check not satisfied, there is addresses, which is not derived from keys" ); } - if !Self::valid_private_key_transaction_pairing_check(&accounts_key_chains) { + if !Self::valid_private_key_transaction_pairing_check(&default_accounts_key_chains) { anyhow::bail!( "Key transaction pairing check not satisfied, there is addresses, which is not derived from keys" ); } Ok(Self { - pub_account_signing_keys: accounts_keys, - user_private_accounts: accounts_key_chains, + default_pub_account_signing_keys: default_accounts_keys, + default_user_private_accounts: default_accounts_key_chains, + public_key_tree, + private_key_tree, password, }) } @@ -109,9 +124,7 @@ impl NSSAUserData { &mut self, parent_cci: ChainIndex, ) -> nssa::Address { - self.public_key_tree - .generate_new_pub_keys(parent_cci) - .unwrap() + self.public_key_tree.generate_new_node(parent_cci).unwrap() } /// Returns the signing key for public transaction signatures @@ -119,35 +132,25 @@ impl NSSAUserData { &self, address: &nssa::Address, ) -> Option<&nssa::PrivateKey> { - self.public_key_tree.get_pub_keys(address) + //First seek in defaults + if let Some(key) = self.default_pub_account_signing_keys.get(address) { + Some(key) + //Then seek in tree + } else { + self.public_key_tree + .get_node(*address) + .and_then(|chain_keys| Some(chain_keys.into())) + } } /// Generated new private key for privacy preserving transactions /// /// Returns the address of new account - pub fn generate_new_privacy_preserving_transaction_key_chain(&mut self) -> nssa::Address { - let key_chain = KeyChain::new_os_random(); - let address = nssa::Address::from(&key_chain.nullifer_public_key); - - self.user_private_accounts - .insert(address, (key_chain, nssa_core::account::Account::default())); - - address - } - - /// Generated new private key for privacy preserving transactions - /// - /// Returns the address of new account - pub fn generate_new_privacy_preserving_transaction_key_chain_mnemonic( + pub fn generate_new_privacy_preserving_transaction_key_chain( &mut self, + parent_cci: ChainIndex, ) -> nssa::Address { - let key_chain = KeyChain::new_mnemonic(self.password.clone()); - let address = nssa::Address::from(&key_chain.nullifer_public_key); - - self.user_private_accounts - .insert(address, (key_chain, nssa_core::account::Account::default())); - - address + self.private_key_tree.generate_new_node(parent_cci).unwrap() } /// Returns the signing key for public transaction signatures @@ -155,7 +158,15 @@ impl NSSAUserData { &self, address: &nssa::Address, ) -> Option<&(KeyChain, nssa_core::account::Account)> { - self.user_private_accounts.get(address) + //First seek in defaults + if let Some(key) = self.default_user_private_accounts.get(address) { + Some(key) + //Then seek in tree + } else { + self.private_key_tree + .get_node(*address) + .and_then(|chain_keys| Some(chain_keys.into())) + } } /// Returns the signing key for public transaction signatures @@ -163,14 +174,27 @@ impl NSSAUserData { &mut self, address: &nssa::Address, ) -> Option<&mut (KeyChain, nssa_core::account::Account)> { - self.user_private_accounts.get_mut(address) + //First seek in defaults + if let Some(key) = self.default_user_private_accounts.get_mut(address) { + Some(key) + //Then seek in tree + } else { + self.private_key_tree + .get_node_mut(*address) + .and_then(|chain_keys| Some(chain_keys.into())) + } } } impl Default for NSSAUserData { fn default() -> Self { - //Safe unwrap as maps are empty - Self::new_with_accounts(HashMap::default(), HashMap::default()).unwrap() + Self::new_with_accounts( + HashMap::new(), + HashMap::new(), + KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_string())), + KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_string())), + ) + .unwrap() } } @@ -182,8 +206,9 @@ mod tests { fn test_new_account() { let mut user_data = NSSAUserData::default(); - let addr_pub = user_data.generate_new_public_transaction_private_key(); - let addr_private = user_data.generate_new_privacy_preserving_transaction_key_chain(); + let addr_pub = user_data.generate_new_public_transaction_private_key(ChainIndex::root()); + let addr_private = + user_data.generate_new_privacy_preserving_transaction_key_chain(ChainIndex::root()); let is_private_key_generated = user_data.get_pub_account_signing_key(&addr_pub).is_some(); diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage/mod.rs index 4a845af..a1a8517 100644 --- a/wallet/src/chain_storage/mod.rs +++ b/wallet/src/chain_storage/mod.rs @@ -1,7 +1,13 @@ use std::collections::HashMap; use anyhow::Result; -use key_protocol::key_protocol_core::NSSAUserData; +use key_protocol::{ + key_management::{ + key_tree::{KeyTreePrivate, KeyTreePublic}, + secret_holders::SeedHolder, + }, + key_protocol_core::NSSAUserData, +}; use nssa::program::Program; use crate::config::{InitialAccountData, PersistentAccountData, WalletConfig}; @@ -12,7 +18,11 @@ pub struct WalletChainStore { } impl WalletChainStore { - pub fn new(config: WalletConfig) -> Result { + pub fn new( + config: WalletConfig, + persistent_accounts: Vec, + password: String, + ) -> Result { let mut public_init_acc_map = HashMap::new(); let mut private_init_acc_map = HashMap::new(); @@ -32,8 +42,27 @@ impl WalletChainStore { } } + let mut public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone())); + let mut private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password)); + + for pers_acc_data in persistent_accounts { + match pers_acc_data { + PersistentAccountData::Public(data) => { + public_tree.insert(data.address, data.chain_index, data.data); + } + PersistentAccountData::Private(data) => { + private_tree.insert(data.address, data.chain_index, data.data); + } + } + } + Ok(Self { - user_data: NSSAUserData::new_with_accounts(public_init_acc_map, private_init_acc_map)?, + user_data: NSSAUserData::new_with_accounts( + public_init_acc_map, + private_init_acc_map, + public_tree, + private_tree, + )?, wallet_config: config, }) } @@ -43,26 +72,20 @@ impl WalletChainStore { addr: nssa::Address, account: nssa_core::account::Account, ) { - println!("inserting at addres {}, this account {:?}", addr, account); + println!("inserting at address {}, this account {:?}", addr, account); self.user_data - .user_private_accounts - .entry(addr) - .and_modify(|(_, acc)| *acc = account); - } - - pub(crate) fn insert_account_data(&mut self, acc_data: PersistentAccountData) { - match acc_data { - PersistentAccountData::Public(acc_data) => { - self.user_data - .pub_account_signing_keys - .insert(acc_data.address, acc_data.pub_sign_key); - } - PersistentAccountData::Private(acc_data) => { - self.user_data - .user_private_accounts - .insert(acc_data.address, (acc_data.key_chain, acc_data.account)); - } - } + .private_key_tree + .addr_map + .get(&addr) + .and_then(|chain_index| { + Some( + self.user_data + .private_key_tree + .key_map + .entry(chain_index.clone()) + .and_modify(|data| data.value.1 = account), + ) + }); } } @@ -180,6 +203,6 @@ mod tests { fn test_new_initializes_correctly() { let config = create_sample_wallet_config(); - let _ = WalletChainStore::new(config.clone()).unwrap(); + let _ = WalletChainStore::new(config.clone(), vec![], "test_pass".to_string()).unwrap(); } } diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 3ff4470..7574a80 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -1,6 +1,7 @@ use anyhow::Result; use base58::ToBase58; use clap::Subcommand; +use key_protocol::key_management::key_tree::chain_index::ChainIndex; use nssa::{Account, Address, program::Program}; use serde::Serialize; @@ -89,9 +90,15 @@ pub enum AccountSubcommand { #[derive(Subcommand, Debug, Clone)] pub enum NewSubcommand { ///Register new public account - Public {}, + Public { + #[arg(long)] + cci: ChainIndex + }, ///Register new private account - Private {}, + Private { + #[arg(long)] + cci: ChainIndex + }, } impl WalletSubcommand for NewSubcommand { @@ -100,8 +107,8 @@ impl WalletSubcommand for NewSubcommand { wallet_core: &mut WalletCore, ) -> Result { match self { - NewSubcommand::Public {} => { - let addr = wallet_core.create_new_account_public(); + NewSubcommand::Public { cci } => { + let addr = wallet_core.create_new_account_public(cci); println!("Generated new account with addr Public/{addr}"); @@ -111,8 +118,8 @@ impl WalletSubcommand for NewSubcommand { Ok(SubcommandReturnValue::RegisterAccount { addr }) } - NewSubcommand::Private {} => { - let addr = wallet_core.create_new_account_private(); + NewSubcommand::Private { cci } => { + let addr = wallet_core.create_new_account_private(cci); let (key, _) = wallet_core .storage @@ -270,7 +277,8 @@ impl WalletSubcommand for AccountSubcommand { if !wallet_core .storage .user_data - .user_private_accounts + .private_key_tree + .addr_map .is_empty() { parse_block_range( diff --git a/wallet/src/config.rs b/wallet/src/config.rs index bf7f5d2..8fdc450 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -1,4 +1,9 @@ -use key_protocol::key_management::KeyChain; +use key_protocol::key_management::{ + KeyChain, + key_tree::{ + chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, + }, +}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -10,7 +15,8 @@ pub struct InitialAccountDataPublic { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersistentAccountDataPublic { pub address: nssa::Address, - pub pub_sign_key: nssa::PrivateKey, + pub chain_index: ChainIndex, + pub data: ChildKeysPublic, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -23,8 +29,8 @@ pub struct InitialAccountDataPrivate { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersistentAccountDataPrivate { pub address: nssa::Address, - pub account: nssa_core::account::Account, - pub key_chain: KeyChain, + pub chain_index: ChainIndex, + pub data: ChildKeysPrivate, } //Big difference in enum variants sizes @@ -48,6 +54,7 @@ pub enum PersistentAccountData { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersistentStorage { pub accounts: Vec, + pub password: String, pub last_synced_block: u64, } diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index f959d17..b3e225d 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -104,6 +104,7 @@ pub async fn fetch_persistent_storage() -> Result { Err(err) => match err.kind() { std::io::ErrorKind::NotFound => Ok(PersistentStorage { accounts: vec![], + password: "default".to_string(), last_synced_block: 0, }), _ => { @@ -120,29 +121,29 @@ pub fn produce_data_for_storage( ) -> PersistentStorage { let mut vec_for_storage = vec![]; - for (addr, key) in &user_data.pub_account_signing_keys { - vec_for_storage.push( - PersistentAccountDataPublic { + for (addr, key) in &user_data.public_key_tree.addr_map { + if let Some(data) = user_data.public_key_tree.key_map.get(key) { + vec_for_storage.push(PersistentAccountDataPublic { address: *addr, - pub_sign_key: key.clone(), - } - .into(), - ); + chain_index: key.clone(), + data: data.clone(), + }.into()); + } } - for (addr, (key, acc)) in &user_data.user_private_accounts { - vec_for_storage.push( - PersistentAccountDataPrivate { + for (addr, key) in &user_data.private_key_tree.addr_map { + if let Some(data) = user_data.private_key_tree.key_map.get(key) { + vec_for_storage.push(PersistentAccountDataPrivate { address: *addr, - account: acc.clone(), - key_chain: key.clone(), - } - .into(), - ); + chain_index: key.clone(), + data: data.clone(), + }.into()); + } } PersistentStorage { accounts: vec_for_storage, + password: user_data.password.clone(), last_synced_block, } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index dd6dd32..ba2570a 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -10,6 +10,7 @@ use common::{ use anyhow::Result; use chain_storage::WalletChainStore; use config::WalletConfig; +use key_protocol::key_management::key_tree::chain_index::ChainIndex; use log::info; use nssa::{ Account, Address, privacy_preserving_transaction::message::EncryptedAccountData, @@ -59,15 +60,13 @@ impl WalletCore { let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); let tx_poller = TxPoller::new(config.clone(), client.clone()); - let mut storage = WalletChainStore::new(config)?; - let PersistentStorage { accounts: persistent_accounts, + password, last_synced_block, } = fetch_persistent_storage().await?; - for pers_acc_data in persistent_accounts { - storage.insert_account_data(pers_acc_data); - } + + let storage = WalletChainStore::new(config, persistent_accounts, password)?; Ok(Self { storage, @@ -107,16 +106,16 @@ impl WalletCore { Ok(config_path) } - pub fn create_new_account_public(&mut self) -> Address { + pub fn create_new_account_public(&mut self, chain_index: ChainIndex) -> Address { self.storage .user_data - .generate_new_public_transaction_private_key() + .generate_new_public_transaction_private_key(chain_index) } - pub fn create_new_account_private(&mut self) -> Address { + pub fn create_new_account_private(&mut self, chain_index: ChainIndex) -> Address { self.storage .user_data - .generate_new_privacy_preserving_transaction_key_chain() + .generate_new_privacy_preserving_transaction_key_chain(chain_index) } ///Get account balance @@ -146,13 +145,12 @@ impl WalletCore { pub fn get_account_private(&self, addr: &Address) -> Option { self.storage .user_data - .user_private_accounts - .get(addr) + .get_private_account(addr) .map(|value| value.1.clone()) } pub fn get_private_account_commitment(&self, addr: &Address) -> Option { - let (keys, account) = self.storage.user_data.user_private_accounts.get(addr)?; + let (keys, account) = self.storage.user_data.get_private_account(addr)?; Some(Commitment::new(&keys.nullifer_public_key, account)) } From ec149d3227c718c0db80da17a0b8b9adf8e0ac06 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 11 Nov 2025 12:15:20 +0200 Subject: [PATCH 06/90] feat: deterministic keys --- integration_tests/Cargo.toml | 3 + integration_tests/src/lib.rs | 2 + integration_tests/src/test_suite_map.rs | 143 ++++++++++------- key_protocol/Cargo.toml | 1 + .../key_management/key_tree/chain_index.rs | 89 ++++++----- .../src/key_management/key_tree/mod.rs | 46 ++++-- .../src/key_management/secret_holders.rs | 10 ++ key_protocol/src/key_protocol_core/mod.rs | 46 +----- wallet/src/chain_storage/mod.rs | 147 ++++++++++++++---- wallet/src/cli/account.rs | 6 +- wallet/src/config.rs | 9 +- wallet/src/helperfunctions.rs | 57 +++++-- wallet/src/lib.rs | 97 +++++++++++- wallet/src/main.rs | 13 +- 14 files changed, 456 insertions(+), 213 deletions(-) diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index c10869a..58be5a5 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -36,6 +36,9 @@ path = "../wallet" [dependencies.common] path = "../common" +[dependencies.key_protocol] +path = "../key_protocol" + [dependencies.nssa] path = "../nssa" features = ["no_docker"] diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index f718f9d..ae32f79 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -54,6 +54,8 @@ fn make_private_account_input_from_str(addr: &str) -> String { pub async fn pre_test( home_dir: PathBuf, ) -> Result<(ServerHandle, JoinHandle>, TempDir)> { + wallet::execute_setup("test_pass".to_string()).await?; + let home_dir_sequencer = home_dir.join("sequencer"); let mut sequencer_config = diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index afcac7d..e4510ad 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1,6 +1,7 @@ use std::{collections::HashMap, path::PathBuf, pin::Pin, time::Duration}; use common::{PINATA_BASE58, sequencer_client::SequencerClient}; +use key_protocol::key_management::key_tree::chain_index::ChainIndex; use log::info; use nssa::{Address, ProgramDeploymentTransaction, program::Program}; use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; @@ -13,7 +14,7 @@ use wallet::{ pinata_program::PinataProgramAgnosticSubcommand, token_program::TokenProgramAgnosticSubcommand, }, - config::{PersistentAccountData, PersistentStorage}, + config::PersistentStorage, helperfunctions::{fetch_config, fetch_persistent_storage}, }; @@ -72,7 +73,9 @@ 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 {})); + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: ChainIndex::root(), + })); let wallet_config = fetch_config().await.unwrap(); @@ -273,47 +276,43 @@ 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( - NewSubcommand::Public {}, + let SubcommandReturnValue::RegisterAccount { + addr: definition_addr, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public { + cci: ChainIndex::root(), + }, ))) .await - .unwrap(); + .unwrap() + else { + panic!("invalid subcommand return value"); + }; // Create new account for the token supply holder - wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public {}, - ))) - .await - .unwrap(); + let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public { + cci: ChainIndex::root(), + }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; // Create new account for receiving a token transaction - wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public {}, + let SubcommandReturnValue::RegisterAccount { + addr: recipient_addr, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public { + cci: ChainIndex::root(), + }, ))) .await - .unwrap(); - - let PersistentStorage { - accounts: persistent_accounts, - last_synced_block: _, - } = fetch_persistent_storage().await.unwrap(); - - let mut new_persistent_accounts_addr = Vec::new(); - - for per_acc in persistent_accounts { - match per_acc { - PersistentAccountData::Public(per_acc) => { - if (per_acc.address.to_string() != ACC_RECEIVER) - && (per_acc.address.to_string() != ACC_SENDER) - { - new_persistent_accounts_addr.push(per_acc.address); - } - } - _ => continue, - } - } - - let [definition_addr, supply_addr, recipient_addr] = new_persistent_accounts_addr - .try_into() - .expect("Failed to produce new account, not present in persistent accounts"); + .unwrap() + else { + panic!("invalid subcommand return value"); + }; // Create new token let subcommand = TokenProgramAgnosticSubcommand::New { @@ -433,7 +432,9 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { addr: definition_addr, } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public {}, + NewSubcommand::Public { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -443,7 +444,9 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private {}, + NewSubcommand::Private { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -454,7 +457,9 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { addr: recipient_addr, } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private {}, + NewSubcommand::Private { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -584,7 +589,9 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { addr: definition_addr, } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public {}, + NewSubcommand::Public { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -594,7 +601,9 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private {}, + NewSubcommand::Private { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -605,7 +614,9 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { addr: recipient_addr, } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private {}, + NewSubcommand::Private { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -716,7 +727,9 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { addr: definition_addr, } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public {}, + NewSubcommand::Public { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -726,7 +739,9 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (public) let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public {}, + NewSubcommand::Public { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -737,7 +752,9 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { addr: recipient_addr, } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private {}, + NewSubcommand::Private { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -847,7 +864,9 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { addr: definition_addr, } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public {}, + NewSubcommand::Public { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -857,7 +876,9 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private {}, + NewSubcommand::Private { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -868,7 +889,9 @@ pub fn prepare_function_map() -> HashMap { let SubcommandReturnValue::RegisterAccount { addr: recipient_addr, } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Public {}, + NewSubcommand::Public { + cci: ChainIndex::root(), + }, ))) .await .unwrap() @@ -1066,7 +1089,9 @@ pub fn prepare_function_map() -> HashMap { ); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); + 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_addr } = sub_ret else { @@ -1082,8 +1107,7 @@ pub fn prepare_function_map() -> HashMap { let (to_keys, _) = wallet_storage .storage .user_data - .user_private_accounts - .get(&to_addr) + .get_private_account(&to_addr) .cloned() .unwrap(); @@ -1134,7 +1158,9 @@ pub fn prepare_function_map() -> HashMap { let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); + 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_addr } = sub_ret else { @@ -1150,8 +1176,7 @@ pub fn prepare_function_map() -> HashMap { let (to_keys, _) = wallet_storage .storage .user_data - .user_private_accounts - .get(&to_addr) + .get_private_account(&to_addr) .cloned() .unwrap(); @@ -1428,7 +1453,9 @@ 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 {})); + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: ChainIndex::root(), + })); let SubcommandReturnValue::RegisterAccount { addr } = wallet::execute_subcommand(command).await.unwrap() else { @@ -1528,7 +1555,9 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { addr: winner_addr } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( - NewSubcommand::Private {}, + NewSubcommand::Private { + cci: ChainIndex::root(), + }, ))) .await .unwrap() diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index b0708b4..a562515 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -14,6 +14,7 @@ hex = "0.4.3" aes-gcm.workspace = true bip39.workspace = true hmac-sha512.workspace = true +thiserror.workspace = true nssa-core = { path = "../nssa/core", features = ["host"] } [dependencies.common] 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 dad9b2a..e22abf0 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -1,59 +1,57 @@ -use std::str::FromStr; +use std::{fmt::Display, str::FromStr}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] pub struct ChainIndex(Vec); +#[derive(thiserror::Error, Debug)] +pub enum ChainIndexError { + #[error("No root found")] + NoRootFound, + #[error("Failed to parse segment into a number")] + ParseIntError(#[from] std::num::ParseIntError), +} + impl FromStr for ChainIndex { - type Err = hex::FromHexError; + type Err = ChainIndexError; fn from_str(s: &str) -> Result { - if s.is_empty() { - return Ok(Self(vec![])); + if !s.starts_with("/") { + return Err(ChainIndexError::NoRootFound); } - let hex_decoded = hex::decode(s)?; - - if !hex_decoded.len().is_multiple_of(4) { - Err(hex::FromHexError::InvalidStringLength) - } else { - let mut res_vec = vec![]; - - for i in 0..(hex_decoded.len() / 4) { - res_vec.push(u32::from_le_bytes([ - hex_decoded[4 * i], - hex_decoded[4 * i + 1], - hex_decoded[4 * i + 2], - hex_decoded[4 * i + 3], - ])); - } - - Ok(Self(res_vec)) + if s == "/" { + return Ok(ChainIndex(vec![])); } + + let uprooted_substring = s.strip_prefix("/").unwrap(); + + let splitted_chain: Vec<&str> = uprooted_substring.split("/").collect(); + let mut res = vec![]; + + for split_ch in splitted_chain { + let cci = split_ch.parse()?; + res.push(cci); + } + + Ok(Self(res)) } } -#[allow(clippy::to_string_trait_impl)] -impl ToString for ChainIndex { - fn to_string(&self) -> String { - if self.0.is_empty() { - return "".to_string(); +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}/")?; } - - let mut res_vec = vec![]; - - for index in &self.0 { - res_vec.extend_from_slice(&index.to_le_bytes()); - } - - hex::encode(res_vec) + write!(f, "{}", self.0.last().unwrap()) } } impl ChainIndex { pub fn root() -> Self { - ChainIndex::from_str("").unwrap() + ChainIndex::from_str("/").unwrap() } pub fn chain(&self) -> &[u32] { @@ -85,31 +83,40 @@ mod tests { #[test] fn test_chain_id_root_correct() { let chain_id = ChainIndex::root(); - let chain_id_2 = ChainIndex::from_str("").unwrap(); + let chain_id_2 = ChainIndex::from_str("/").unwrap(); assert_eq!(chain_id, chain_id_2); } #[test] fn test_chain_id_deser_correct() { - let chain_id = ChainIndex::from_str("01010000").unwrap(); + let chain_id = ChainIndex::from_str("/257").unwrap(); assert_eq!(chain_id.chain(), &[257]); } #[test] fn test_chain_id_next_in_line_correct() { - let chain_id = ChainIndex::from_str("01010000").unwrap(); + let chain_id = ChainIndex::from_str("/257").unwrap(); let next_in_line = chain_id.next_in_line(); - assert_eq!(next_in_line, ChainIndex::from_str("02010000").unwrap()); + assert_eq!(next_in_line, ChainIndex::from_str("/258").unwrap()); } #[test] fn test_chain_id_child_correct() { - let chain_id = ChainIndex::from_str("01010000").unwrap(); + let chain_id = ChainIndex::from_str("/257").unwrap(); let child = chain_id.n_th_child(3); - assert_eq!(child, ChainIndex::from_str("0101000003000000").unwrap()); + assert_eq!(child, ChainIndex::from_str("/257/3").unwrap()); + } + + #[test] + fn test_correct_display() { + let chainid = ChainIndex(vec![5, 7, 8]); + + let string_index = format!("{chainid}"); + + assert_eq!(string_index, "/5/7/8".to_string()); } } diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 7ea2a2a..dcc027b 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -40,6 +40,18 @@ impl KeyTree { Self { key_map, addr_map } } + pub fn new_from_root(root: Node) -> Self { + let mut key_map = BTreeMap::new(); + let mut addr_map = HashMap::new(); + + addr_map.insert(root.address(), ChainIndex::root()); + key_map.insert(ChainIndex::root(), root); + + Self { key_map, addr_map } + } + + //ToDo: Add function to create a tree from list of nodes with consistency check. + pub fn find_next_last_child_of_id(&self, parent_id: &ChainIndex) -> Option { if !self.key_map.contains_key(parent_id) { return None; @@ -160,7 +172,7 @@ mod tests { assert!( tree.key_map - .contains_key(&ChainIndex::from_str("00000000").unwrap()) + .contains_key(&ChainIndex::from_str("/0").unwrap()) ); let next_last_child_for_parent_id = tree @@ -199,7 +211,7 @@ mod tests { assert!( tree.key_map - .contains_key(&ChainIndex::from_str("00000000").unwrap()) + .contains_key(&ChainIndex::from_str("/0").unwrap()) ); let next_last_child_for_parent_id = tree @@ -208,7 +220,7 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 1); - let key_opt = tree.generate_new_node(ChainIndex::from_str("03000000").unwrap()); + let key_opt = tree.generate_new_node(ChainIndex::from_str("/3").unwrap()); assert_eq!(key_opt, None); } @@ -229,7 +241,7 @@ mod tests { assert!( tree.key_map - .contains_key(&ChainIndex::from_str("00000000").unwrap()) + .contains_key(&ChainIndex::from_str("/0").unwrap()) ); let next_last_child_for_parent_id = tree @@ -242,7 +254,7 @@ mod tests { assert!( tree.key_map - .contains_key(&ChainIndex::from_str("01000000").unwrap()) + .contains_key(&ChainIndex::from_str("/1").unwrap()) ); let next_last_child_for_parent_id = tree @@ -251,58 +263,58 @@ mod tests { assert_eq!(next_last_child_for_parent_id, 2); - tree.generate_new_node(ChainIndex::from_str("00000000").unwrap()) + tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree - .find_next_last_child_of_id(&ChainIndex::from_str("00000000").unwrap()) + .find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap()) .unwrap(); assert_eq!(next_last_child_for_parent_id, 1); assert!( tree.key_map - .contains_key(&ChainIndex::from_str("0000000000000000").unwrap()) + .contains_key(&ChainIndex::from_str("/0/0").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("00000000").unwrap()) + tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree - .find_next_last_child_of_id(&ChainIndex::from_str("00000000").unwrap()) + .find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap()) .unwrap(); assert_eq!(next_last_child_for_parent_id, 2); assert!( tree.key_map - .contains_key(&ChainIndex::from_str("0000000001000000").unwrap()) + .contains_key(&ChainIndex::from_str("/0/1").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("00000000").unwrap()) + tree.generate_new_node(ChainIndex::from_str("/0").unwrap()) .unwrap(); let next_last_child_for_parent_id = tree - .find_next_last_child_of_id(&ChainIndex::from_str("00000000").unwrap()) + .find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap()) .unwrap(); assert_eq!(next_last_child_for_parent_id, 3); assert!( tree.key_map - .contains_key(&ChainIndex::from_str("0000000002000000").unwrap()) + .contains_key(&ChainIndex::from_str("/0/2").unwrap()) ); - tree.generate_new_node(ChainIndex::from_str("0000000001000000").unwrap()) + tree.generate_new_node(ChainIndex::from_str("/0/1").unwrap()) .unwrap(); assert!( tree.key_map - .contains_key(&ChainIndex::from_str("000000000100000000000000").unwrap()) + .contains_key(&ChainIndex::from_str("/0/1/0").unwrap()) ); let next_last_child_for_parent_id = tree - .find_next_last_child_of_id(&ChainIndex::from_str("0000000001000000").unwrap()) + .find_next_last_child_of_id(&ChainIndex::from_str("/0/1").unwrap()) .unwrap(); assert_eq!(next_last_child_for_parent_id, 1); diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index ee7aced..e60a9f5 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -166,4 +166,14 @@ mod tests { let _ = top_secret_key_holder.generate_outgoing_viewing_secret_key(); } + + #[test] + fn two_seeds_generated_same_from_same_mnemonic() { + let mnemonic = "test_pass"; + + let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_string()); + let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_string()); + + assert_eq!(seed_holder1.seed, seed_holder2.seed); + } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index f50088a..6ea75db 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -19,8 +19,6 @@ pub struct NSSAUserData { ///Default private accounts pub default_user_private_accounts: HashMap, - ///Mnemonic passphrase - pub password: String, /// Tree of public keys pub public_key_tree: KeyTreePublic, /// Tree of private keys @@ -82,38 +80,6 @@ impl NSSAUserData { default_user_private_accounts: default_accounts_key_chains, public_key_tree, private_key_tree, - password: "mnemonic".to_string(), - }) - } - - pub fn new_with_accounts_and_password( - default_accounts_keys: HashMap, - default_accounts_key_chains: HashMap< - nssa::Address, - (KeyChain, nssa_core::account::Account), - >, - public_key_tree: KeyTreePublic, - private_key_tree: KeyTreePrivate, - password: String, - ) -> Result { - if !Self::valid_public_key_transaction_pairing_check(&default_accounts_keys) { - anyhow::bail!( - "Key transaction pairing check not satisfied, there is addresses, which is not derived from keys" - ); - } - - if !Self::valid_private_key_transaction_pairing_check(&default_accounts_key_chains) { - anyhow::bail!( - "Key transaction pairing check not satisfied, there is addresses, which is not derived from keys" - ); - } - - Ok(Self { - default_pub_account_signing_keys: default_accounts_keys, - default_user_private_accounts: default_accounts_key_chains, - public_key_tree, - private_key_tree, - password, }) } @@ -137,9 +103,7 @@ impl NSSAUserData { Some(key) //Then seek in tree } else { - self.public_key_tree - .get_node(*address) - .and_then(|chain_keys| Some(chain_keys.into())) + self.public_key_tree.get_node(*address).map(Into::into) } } @@ -163,9 +127,7 @@ impl NSSAUserData { Some(key) //Then seek in tree } else { - self.private_key_tree - .get_node(*address) - .and_then(|chain_keys| Some(chain_keys.into())) + self.private_key_tree.get_node(*address).map(Into::into) } } @@ -179,9 +141,7 @@ impl NSSAUserData { Some(key) //Then seek in tree } else { - self.private_key_tree - .get_node_mut(*address) - .and_then(|chain_keys| Some(chain_keys.into())) + self.private_key_tree.get_node_mut(*address).map(Into::into) } } } diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage/mod.rs index a1a8517..5215d99 100644 --- a/wallet/src/chain_storage/mod.rs +++ b/wallet/src/chain_storage/mod.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use anyhow::Result; use key_protocol::{ key_management::{ - key_tree::{KeyTreePrivate, KeyTreePublic}, + key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, secret_holders::SeedHolder, }, key_protocol_core::NSSAUserData, @@ -21,8 +21,73 @@ impl WalletChainStore { pub fn new( config: WalletConfig, persistent_accounts: Vec, - password: String, ) -> Result { + if persistent_accounts.is_empty() { + anyhow::bail!("Roots not found; please run setup beforehand"); + } + + let mut public_init_acc_map = HashMap::new(); + let mut private_init_acc_map = HashMap::new(); + + let public_root = persistent_accounts + .iter() + .find(|data| match data { + &PersistentAccountData::Public(data) => data.chain_index == ChainIndex::root(), + _ => false, + }) + .cloned() + .unwrap(); + + let private_root = persistent_accounts + .iter() + .find(|data| match data { + &PersistentAccountData::Private(data) => data.chain_index == ChainIndex::root(), + _ => false, + }) + .cloned() + .unwrap(); + + let mut public_tree = KeyTreePublic::new_from_root(match public_root { + PersistentAccountData::Public(data) => data.data, + _ => unreachable!(), + }); + let mut private_tree = KeyTreePrivate::new_from_root(match private_root { + PersistentAccountData::Private(data) => data.data, + _ => unreachable!(), + }); + + for pers_acc_data in persistent_accounts { + match pers_acc_data { + PersistentAccountData::Public(data) => { + public_tree.insert(data.address, data.chain_index, data.data); + } + PersistentAccountData::Private(data) => { + private_tree.insert(data.address, data.chain_index, data.data); + } + PersistentAccountData::Preconfigured(acc_data) => match acc_data { + InitialAccountData::Public(data) => { + public_init_acc_map.insert(data.address.parse()?, data.pub_sign_key); + } + InitialAccountData::Private(data) => { + private_init_acc_map + .insert(data.address.parse()?, (data.key_chain, data.account)); + } + }, + } + } + + Ok(Self { + user_data: NSSAUserData::new_with_accounts( + public_init_acc_map, + private_init_acc_map, + public_tree, + private_tree, + )?, + wallet_config: config, + }) + } + + pub fn new_storage(config: WalletConfig, password: String) -> Result { let mut public_init_acc_map = HashMap::new(); let mut private_init_acc_map = HashMap::new(); @@ -42,19 +107,8 @@ impl WalletChainStore { } } - let mut public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone())); - let mut private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password)); - - for pers_acc_data in persistent_accounts { - match pers_acc_data { - PersistentAccountData::Public(data) => { - public_tree.insert(data.address, data.chain_index, data.data); - } - PersistentAccountData::Private(data) => { - private_tree.insert(data.address, data.chain_index, data.data); - } - } - } + let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone())); + let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password)); Ok(Self { user_data: NSSAUserData::new_with_accounts( @@ -73,25 +127,41 @@ impl WalletChainStore { account: nssa_core::account::Account, ) { println!("inserting at address {}, this account {:?}", addr, account); - self.user_data - .private_key_tree - .addr_map - .get(&addr) - .and_then(|chain_index| { - Some( + + if self + .user_data + .default_user_private_accounts + .contains_key(&addr) + { + self.user_data + .default_user_private_accounts + .entry(addr) + .and_modify(|data| data.1 = account); + } else { + self.user_data + .private_key_tree + .addr_map + .get(&addr) + .map(|chain_index| { self.user_data .private_key_tree .key_map .entry(chain_index.clone()) - .and_modify(|data| data.value.1 = account), - ) - }); + .and_modify(|data| data.value.1 = account) + }); + } } } #[cfg(test)] mod tests { - use crate::config::InitialAccountData; + use key_protocol::key_management::key_tree::{ + keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, traits::KeyNode, + }; + + use crate::config::{ + InitialAccountData, PersistentAccountDataPrivate, PersistentAccountDataPublic, + }; use super::*; @@ -199,10 +269,35 @@ mod tests { } } + fn create_sample_persistent_accounts() -> Vec { + let mut accs = vec![]; + + let public_data = ChildKeysPublic::root([42; 64]); + + accs.push(PersistentAccountData::Public(PersistentAccountDataPublic { + address: public_data.address(), + chain_index: ChainIndex::root(), + data: public_data, + })); + + let private_data = ChildKeysPrivate::root([47; 64]); + + accs.push(PersistentAccountData::Private( + PersistentAccountDataPrivate { + address: private_data.address(), + chain_index: ChainIndex::root(), + data: private_data, + }, + )); + + accs + } + #[test] fn test_new_initializes_correctly() { let config = create_sample_wallet_config(); + let accs = create_sample_persistent_accounts(); - let _ = WalletChainStore::new(config.clone(), vec![], "test_pass".to_string()).unwrap(); + let _ = WalletChainStore::new(config.clone(), accs).unwrap(); } } diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 7574a80..b9a7699 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -90,14 +90,14 @@ pub enum AccountSubcommand { #[derive(Subcommand, Debug, Clone)] pub enum NewSubcommand { ///Register new public account - Public { + Public { #[arg(long)] - cci: ChainIndex + cci: ChainIndex, }, ///Register new private account Private { #[arg(long)] - cci: ChainIndex + cci: ChainIndex, }, } diff --git a/wallet/src/config.rs b/wallet/src/config.rs index 8fdc450..f5bd3ea 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -49,12 +49,12 @@ pub enum InitialAccountData { pub enum PersistentAccountData { Public(PersistentAccountDataPublic), Private(PersistentAccountDataPrivate), + Preconfigured(InitialAccountData), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersistentStorage { pub accounts: Vec, - pub password: String, pub last_synced_block: u64, } @@ -72,6 +72,7 @@ impl PersistentAccountData { match &self { Self::Public(acc) => acc.address, Self::Private(acc) => acc.address, + Self::Preconfigured(acc) => acc.address(), } } } @@ -100,6 +101,12 @@ impl From for PersistentAccountData { } } +impl From for PersistentAccountData { + fn from(value: InitialAccountData) -> Self { + Self::Preconfigured(value) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GasConfig { /// Gas spent per deploying one byte of data diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index b3e225d..0d08348 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -12,6 +12,7 @@ use serde::Serialize; use crate::{ HOME_DIR_ENV_VAR, config::{ + InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig, }, }; @@ -102,11 +103,9 @@ pub async fn fetch_persistent_storage() -> Result { Ok(serde_json::from_slice(&storage_content)?) } Err(err) => match err.kind() { - std::io::ErrorKind::NotFound => Ok(PersistentStorage { - accounts: vec![], - password: "default".to_string(), - last_synced_block: 0, - }), + std::io::ErrorKind::NotFound => { + anyhow::bail!("Not found, please setup roots from config command beforehand"); + } _ => { anyhow::bail!("IO error {err:#?}"); } @@ -123,27 +122,53 @@ pub fn produce_data_for_storage( for (addr, key) in &user_data.public_key_tree.addr_map { if let Some(data) = user_data.public_key_tree.key_map.get(key) { - vec_for_storage.push(PersistentAccountDataPublic { - address: *addr, - chain_index: key.clone(), - data: data.clone(), - }.into()); + vec_for_storage.push( + PersistentAccountDataPublic { + address: *addr, + chain_index: key.clone(), + data: data.clone(), + } + .into(), + ); } } for (addr, key) in &user_data.private_key_tree.addr_map { if let Some(data) = user_data.private_key_tree.key_map.get(key) { - vec_for_storage.push(PersistentAccountDataPrivate { - address: *addr, - chain_index: key.clone(), - data: data.clone(), - }.into()); + vec_for_storage.push( + PersistentAccountDataPrivate { + address: *addr, + chain_index: key.clone(), + data: data.clone(), + } + .into(), + ); } } + for (addr, key) in &user_data.default_pub_account_signing_keys { + vec_for_storage.push( + InitialAccountData::Public(InitialAccountDataPublic { + address: addr.to_string(), + pub_sign_key: key.clone(), + }) + .into(), + ) + } + + for (addr, (key_chain, account)) in &user_data.default_user_private_accounts { + vec_for_storage.push( + InitialAccountData::Private(InitialAccountDataPrivate { + address: addr.to_string(), + account: account.clone(), + key_chain: key_chain.clone(), + }) + .into(), + ) + } + PersistentStorage { accounts: vec_for_storage, - password: user_data.password.clone(), last_synced_block, } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index ba2570a..3b494a9 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -10,7 +10,7 @@ use common::{ use anyhow::Result; use chain_storage::WalletChainStore; use config::WalletConfig; -use key_protocol::key_management::key_tree::chain_index::ChainIndex; +use key_protocol::key_management::key_tree::{chain_index::ChainIndex, traits::KeyNode}; use log::info; use nssa::{ Account, Address, privacy_preserving_transaction::message::EncryptedAccountData, @@ -62,11 +62,10 @@ impl WalletCore { let PersistentStorage { accounts: persistent_accounts, - password, last_synced_block, } = fetch_persistent_storage().await?; - let storage = WalletChainStore::new(config, persistent_accounts, password)?; + let storage = WalletChainStore::new(config, persistent_accounts)?; Ok(Self { storage, @@ -76,6 +75,23 @@ impl WalletCore { }) } + pub async fn start_from_config_new_storage( + config: WalletConfig, + password: String, + ) -> Result { + let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); + let tx_poller = TxPoller::new(config.clone(), client.clone()); + + let storage = WalletChainStore::new_storage(config, password)?; + + Ok(Self { + storage, + poller: tx_poller, + sequencer_client: client.clone(), + last_synced_block: 0, + }) + } + ///Store persistent data at home pub async fn store_persistent_data(&self) -> Result { let home = get_home()?; @@ -233,6 +249,18 @@ pub enum Command { Config(ConfigSubcommand), } +///Represents CLI command for a wallet with setup included +#[derive(Debug, Subcommand, Clone)] +#[clap(about)] +pub enum OverCommand { + #[command(subcommand)] + Command(Command), + Setup { + #[arg(short, long)] + password: String, + }, +} + ///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. @@ -247,7 +275,7 @@ pub struct Args { pub continious_run: bool, /// Wallet command #[command(subcommand)] - pub command: Option, + pub command: Option, } #[derive(Debug, Clone)] @@ -341,8 +369,11 @@ pub async fn parse_block_range( if let NSSATransaction::PrivacyPreserving(tx) = nssa_tx { let mut affected_accounts = vec![]; - for (acc_addr, (key_chain, _)) in - &wallet_core.storage.user_data.user_private_accounts + for (acc_addr, (key_chain, _)) in wallet_core + .storage + .user_data + .default_user_private_accounts + .iter() { let view_tag = EncryptedAccountData::compute_view_tag( key_chain.nullifer_public_key.clone(), @@ -379,6 +410,51 @@ pub async fn parse_block_range( } } + for (_, keys_node) in wallet_core + .storage + .user_data + .private_key_tree + .key_map + .iter() + { + let acc_addr = keys_node.address(); + let key_chain = &keys_node.value.0; + + 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 addr {acc_addr:#?} with account object {res_acc:#?}" + ); + + affected_accounts.push((acc_addr, res_acc)); + } + } + } + } + for (affected_addr, new_acc) in affected_accounts { wallet_core .storage @@ -426,3 +502,12 @@ pub async fn execute_continious_run() -> Result<()> { latest_block_num = seq_client.get_last_block().await?.last_block; } } + +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?; + + wallet_core.store_persistent_data().await?; + + Ok(()) +} diff --git a/wallet/src/main.rs b/wallet/src/main.rs index ecc50d2..1fe52b3 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, execute_continious_run, execute_subcommand}; +use wallet::{Args, OverCommand, execute_continious_run, execute_setup, execute_subcommand}; pub const NUM_THREADS: usize = 2; @@ -17,8 +17,15 @@ fn main() -> Result<()> { env_logger::init(); runtime.block_on(async move { - if let Some(command) = args.command { - execute_subcommand(command).await.unwrap(); + if let Some(overcommand) = args.command { + match overcommand { + OverCommand::Command(command) => { + execute_subcommand(command).await.unwrap(); + } + OverCommand::Setup { password } => { + execute_setup(password).await.unwrap(); + } + } } else if args.continious_run { execute_continious_run().await.unwrap(); } else { From fcb3993a4780458a455615448b551cad58efb3f4 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 11 Nov 2025 15:41:32 +0200 Subject: [PATCH 07/90] fix: merge fix --- integration_tests/Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 11d7d07..58be5a5 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -42,6 +42,3 @@ path = "../key_protocol" [dependencies.nssa] path = "../nssa" features = ["no_docker"] - -[dependencies.key_protocol] -path = "../key_protocol" From 2e582e7874eb8c164d3753f4f7652bda6c718a79 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 12 Nov 2025 19:08:46 -0300 Subject: [PATCH 08/90] add multi chain calls --- nssa/core/src/program.rs | 6 +++--- .../src/bin/privacy_preserving_circuit.rs | 2 +- nssa/src/public_transaction/transaction.rs | 5 ++++- nssa/src/state.rs | 5 +++-- .../guest/src/bin/chain_caller.rs | 19 +++++++++++++------ 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 3ecee30..d36cf8f 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -25,7 +25,7 @@ pub struct ChainedCall { pub struct ProgramOutput { pub pre_states: Vec, pub post_states: Vec, - pub chained_call: Option, + pub chained_call: Vec, } pub fn read_nssa_inputs() -> ProgramInput { @@ -42,7 +42,7 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec let output = ProgramOutput { pre_states, post_states, - chained_call: None, + chained_call: Vec::new(), }; env::commit(&output); } @@ -50,7 +50,7 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec pub fn write_nssa_outputs_with_chained_call( pre_states: Vec, post_states: Vec, - chained_call: Option, + chained_call: Vec, ) { let output = ProgramOutput { pre_states, diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index d8ed15d..530c87e 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -31,7 +31,7 @@ fn main() { } = program_output; // TODO: implement chained calls for privacy preserving transactions - if chained_call.is_some() { + if !chained_call.is_empty() { panic!("Privacy preserving transactions do not support yet chained calls.") } diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index d118d0c..cce4ffd 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -105,6 +105,7 @@ impl PublicTransaction { let mut program_id = message.program_id; let mut instruction_data = message.instruction_data.clone(); + let mut chained_calls = Vec::new(); for _i in 0..MAX_NUMBER_CHAINED_CALLS { // Check the `program_id` corresponds to a deployed program @@ -147,7 +148,9 @@ impl PublicTransaction { state_diff.insert(pre.account_id, post.clone()); } - if let Some(next_chained_call) = program_output.chained_call { + chained_calls.extend_from_slice(&program_output.chained_call); + + if let Some(next_chained_call) = chained_calls.pop() { program_id = next_chained_call.program_id; instruction_data = next_chained_call.instruction_data; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 4120824..b0f60eb 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2096,7 +2096,7 @@ pub mod tests { let expected_to_post = Account { program_owner: Program::chain_caller().id(), - balance: amount, + balance: amount * 2, // The `chain_caller` chains the program twice ..Account::default() }; @@ -2114,7 +2114,8 @@ pub mod tests { let from_post = state.get_account_by_address(&from); let to_post = state.get_account_by_address(&to); - assert_eq!(from_post.balance, initial_balance - amount); + // The `chain_caller` program calls the program twice + assert_eq!(from_post.balance, initial_balance - 2 * amount); assert_eq!(to_post, expected_to_post); } } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index dfd77b1..c4a548b 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -5,7 +5,7 @@ use risc0_zkvm::serde::to_vec; type Instruction = (u128, ProgramId); -/// A program that calls another program. +/// A program that calls another program twice. /// It permutes the order of the input accounts on the subsequent call fn main() { let ProgramInput { @@ -20,11 +20,18 @@ fn main() { let instruction_data = to_vec(&balance).unwrap(); - let chained_call = Some(ChainedCall { - program_id, - instruction_data, - account_indices: vec![1, 0], // <- Account order permutation here - }); + let chained_call = vec![ + ChainedCall { + program_id, + instruction_data: instruction_data.clone(), + account_indices: vec![0, 1], + }, + ChainedCall { + program_id, + instruction_data, + account_indices: vec![1, 0], // <- Account order permutation here + }, + ]; write_nssa_outputs_with_chained_call( vec![sender_pre.clone(), receiver_pre.clone()], From a94440fa1f6ccd030d56c1434755d175cc913338 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 12 Nov 2025 19:18:04 -0300 Subject: [PATCH 09/90] rename --- nssa/core/src/program.rs | 6 +++--- .../guest/src/bin/privacy_preserving_circuit.rs | 4 ++-- nssa/src/public_transaction/transaction.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index d36cf8f..8db2679 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -25,7 +25,7 @@ pub struct ChainedCall { pub struct ProgramOutput { pub pre_states: Vec, pub post_states: Vec, - pub chained_call: Vec, + pub chained_calls: Vec, } pub fn read_nssa_inputs() -> ProgramInput { @@ -42,7 +42,7 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec let output = ProgramOutput { pre_states, post_states, - chained_call: Vec::new(), + chained_calls: Vec::new(), }; env::commit(&output); } @@ -55,7 +55,7 @@ pub fn write_nssa_outputs_with_chained_call( let output = ProgramOutput { pre_states, post_states, - chained_call, + chained_calls: chained_call, }; env::commit(&output); } 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 530c87e..6696245 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -27,11 +27,11 @@ fn main() { let ProgramOutput { pre_states, post_states, - chained_call, + chained_calls, } = program_output; // TODO: implement chained calls for privacy preserving transactions - if !chained_call.is_empty() { + if !chained_calls.is_empty() { panic!("Privacy preserving transactions do not support yet chained calls.") } diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index cce4ffd..cfee8db 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -148,7 +148,7 @@ impl PublicTransaction { state_diff.insert(pre.account_id, post.clone()); } - chained_calls.extend_from_slice(&program_output.chained_call); + chained_calls.extend_from_slice(&program_output.chained_calls); if let Some(next_chained_call) = chained_calls.pop() { program_id = next_chained_call.program_id; From c7b415b2f4b57b46b0544197c7d95c4fd553219d Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 12 Nov 2025 19:55:02 -0300 Subject: [PATCH 10/90] add max depth reached error for chained calls --- nssa/src/error.rs | 3 ++ nssa/src/public_transaction/transaction.rs | 8 +++- nssa/src/state.rs | 45 +++++++++++++++++-- .../guest/src/bin/chain_caller.rs | 22 ++++----- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 8ed9657..2299731 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -54,4 +54,7 @@ pub enum NssaError { #[error("Program already exists")] ProgramAlreadyExists, + + #[error("Chain of calls too long")] + MaxChainedCallsDepthExceeded, } diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index cfee8db..199d60d 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -11,6 +11,7 @@ use crate::{ V02State, error::NssaError, public_transaction::{Message, WitnessSet}, + state::MAX_NUMBER_CHAINED_CALLS, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -18,7 +19,6 @@ pub struct PublicTransaction { message: Message, witness_set: WitnessSet, } -const MAX_NUMBER_CHAINED_CALLS: usize = 10; impl PublicTransaction { pub fn new(message: Message, witness_set: WitnessSet) -> Self { @@ -183,7 +183,11 @@ impl PublicTransaction { }; } - Ok(state_diff) + if chained_calls.is_empty() { + Ok(state_diff) + } else { + Err(NssaError::MaxChainedCallsDepthExceeded) + } } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index b0f60eb..d53609c 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -10,6 +10,8 @@ use nssa_core::{ }; use std::collections::{HashMap, HashSet}; +pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; + pub(crate) struct CommitmentSet { merkle_tree: MerkleTree, commitments: HashMap, @@ -251,6 +253,7 @@ pub mod tests { program::Program, public_transaction, signature::PrivateKey, + state::MAX_NUMBER_CHAINED_CALLS, }; use nssa_core::{ @@ -2079,7 +2082,7 @@ pub mod tests { } #[test] - fn test_chained_call() { + fn test_chained_call_succeeds() { let program = Program::chain_caller(); let key = PrivateKey::try_new([1; 32]).unwrap(); let address = Address::from(&PublicKey::new_from_private_key(&key)); @@ -2091,8 +2094,8 @@ pub mod tests { let from_key = key; let to = Address::new([2; 32]); let amount: u128 = 37; - let instruction: (u128, ProgramId) = - (amount, Program::authenticated_transfer_program().id()); + let instruction: (u128, ProgramId, u32) = + (amount, Program::authenticated_transfer_program().id(), 2); let expected_to_post = Account { program_owner: Program::chain_caller().id(), @@ -2118,4 +2121,40 @@ pub mod tests { assert_eq!(from_post.balance, initial_balance - 2 * amount); assert_eq!(to_post, expected_to_post); } + + #[test] + fn test_execution_fails_if_chained_calls_exceeds_depth() { + let program = Program::chain_caller(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let address = Address::from(&PublicKey::new_from_private_key(&key)); + let initial_balance = 100; + let initial_data = [(address, initial_balance)]; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let from = address; + let from_key = key; + let to = Address::new([2; 32]); + let amount: u128 = 0; + let instruction: (u128, ProgramId, u32) = ( + amount, + Program::authenticated_transfer_program().id(), + MAX_NUMBER_CHAINED_CALLS as u32 + 1, + ); + + let message = public_transaction::Message::try_new( + program.id(), + vec![to, from], //The chain_caller program permutes the account order in the chain call + vec![0], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx); + assert!(matches!( + result, + Err(NssaError::MaxChainedCallsDepthExceeded) + )); + } } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index c4a548b..5ebb6e6 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -3,14 +3,14 @@ use nssa_core::program::{ }; use risc0_zkvm::serde::to_vec; -type Instruction = (u128, ProgramId); +type Instruction = (u128, ProgramId, u32); -/// A program that calls another program twice. +/// A program that calls another program `num_chain_calls` times. /// It permutes the order of the input accounts on the subsequent call fn main() { let ProgramInput { pre_states, - instruction: (balance, program_id), + instruction: (balance, program_id, num_chain_calls), } = read_nssa_inputs::(); let [sender_pre, receiver_pre] = match pre_states.try_into() { @@ -20,19 +20,21 @@ fn main() { let instruction_data = to_vec(&balance).unwrap(); - let chained_call = vec![ + let mut chained_call = vec![ ChainedCall { program_id, instruction_data: instruction_data.clone(), account_indices: vec![0, 1], - }, - ChainedCall { - program_id, - instruction_data, - account_indices: vec![1, 0], // <- Account order permutation here - }, + }; + num_chain_calls as usize - 1 ]; + chained_call.push(ChainedCall { + program_id, + instruction_data, + account_indices: vec![1, 0], // <- Account order permutation here + }); + write_nssa_outputs_with_chained_call( vec![sender_pre.clone(), receiver_pre.clone()], vec![sender_pre.account, receiver_pre.account], From 0ea46f5048d17adb914fecf4c50a17383de20420 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 13 Nov 2025 17:50:15 -0300 Subject: [PATCH 11/90] update Readme --- README.md | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 208 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e89cedd..b7a13d2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,69 @@ -# nescience-testnet -This repo serves for Nescience testnet +# Nescience + +Nescience State Separation Architecture (NSSA) is a programmable blockchain system that introduces a clean separation between public and private states, while keeping them fully interoperable. It lets developers build apps that can operate across both transparent and privacy-preserving accounts without changing how they write or deploy programs. Privacy is handled automatically by the protocol through zero-knowledge proofs (ZKPs). The result is a fully composable blockchain where privacy comes built-in. + +## Background + +Typically, public blockchains maintain a fully transparent state, where the mapping from addresses to account values is entirely visible. In NSSA, we introduce a parallel *private state*, a new layer of accounts that coexists with the public one. The public and private states can be viewed as a partition of the address space: accounts with public addresses are openly visible, while private accounts are accessible only to holders of the corresponding viewing keys. Consistency across both states is enforced through zero-knowledge proofs (ZKPs). + +Public accounts are represented on-chain as a visible map from addresses to account states and are modified in-place when their values change. Private accounts, by contrast, are never stored in raw form on-chain. Each update creates a new commitment, which cryptographically binds the current value of the account while preserving privacy. Commitments of previous valid versions remain on-chain, but a nullifier set is maintained to mark old versions as spent, ensuring that only the most up-to-date version of each private account can be used in any execution. + +### Programmability and selective privacy + +Our goal is to enable full programmability within this hybrid model, matching the flexibility and composability of public blockchains. Developers write and deploy programs in NSSA just as they would on any other blockchain. Privacy, along with the ability to execute programs involving any combination of public and private accounts, is handled entirely at the protocol level and available out of the box for all programs. From the program’s perspective, all accounts are indistinguishable. This abstraction allows developers to focus purely on business logic, while the system transparently enforces privacy and consistency guarantees. + +To the best of our knowledge, this approach is unique to Nescience. Other programmable blockchains with a focus on privacy typically adopt a developer-driven model for private execution, meaning that dApp logic must explicitly handle private inputs correctly. In contrast, Nescience handles privacy at the protocol level, so developers do not need to modify their programs—private and public accounts are treated uniformly, and privacy-preserving execution is available out of the box. + +### Example: creating and transferring tokens across states + +1. Token creation (public execution): + - Alice submits a transaction to execute the token program `Create` function on-chain. + - A new public token account is created, representing the token. + - The minted tokens are recorded on-chain and fully visible on Alice's public account. +2. Transfer from public to private (local / privacy-preserving execution) + - Alice executes the token program `Transfer` function locally, specifying a Bob’s private account as recipient. + - A ZKP of correct execution is generated. + - The proof is submitted to the blockchain, and validator nodes verify it. + - Alice's public account balance is modified accordingly. + - Bob’s private account and balance remain hidden, while the transfer is provably valid. +3. Transferring private to public (local / privacy-preserving execution) + - Bob executes the token program `Transfer` function locally, specifying a Charlie’s public account as recipient. + - A ZKP of correct execution is generated. + - Bob’s private account and balance still remain hidden. + - Charlie's public account is modified with the new tokens added. +4. Transferring public to public (public execution): + - Alice submits a transaction to execute the token program `Transfer` function on-chain, specifying Charlie's public account as recipient. + - The execution is handled on-chain without ZKPs involved. + - Alice's and Charlie's accounts are modified according to the transaction. + +#### Key points: +- The same token program is used in all executions. +- The difference lies in execution mode: public executions update visible accounts on-chain, while private executions rely on ZKPs. +- Validators only need to verify proofs for privacy-preserving transactions, keeping processing efficient. + +### The account’s model + +To achieve both state separation and full programmability, NSSA adopts a stateless program model. Programs do not hold internal state. Instead, all persistent data resides in accounts explicitly passed to the program during execution. This design enables fine-grained control over access and visibility while maintaining composability across public and private states. + +### Execution types + +Execution is divided into two fundamentally distinct types based on how they are processed: public execution, which is executed transparently on-chain, and private execution, which occurs off-chain. For private execution, the blockchain relies on ZKPs to verify the correctness of execution and ensure that all system invariants are preserved. + +Both public and private executions of the same program are enforced to use the same Risc0 VM bytecode. For public transactions, programs are executed directly on-chain like any standard RISC-V VM execution, without generating or verifying proofs. For privacy-preserving transactions, users generate Risc0 ZKPs of correct execution, and validator nodes only verify these proofs rather than re-executing the program. This design ensures that from a validator’s perspective, public transactions are processed as quickly as any RISC-V–based VM, while verification of ZKPs keeps privacy-preserving transactions efficient as well. Additionally, the system naturally supports parallel execution similar to Solana, further increasing throughput. The main computational bottleneck for privacy-preserving transactions lies on the user side, in generating zk proofs. + +### Resources +- [IFT Research call](https://forum.vac.dev/t/ift-research-call-september-10th-2025-updates-on-the-development-of-nescience/566) +- [NSSA v0.2 specs](https://www.notion.so/NSSA-v0-2-specifications-2848f96fb65c800c9818e6f66d9be8f2) +- [Choice of VM/zkVM](https://www.notion.so/Conclusion-on-the-chosen-VM-and-zkVM-for-NSSA-2318f96fb65c806a810ed1300f56992d) +- [NSSA vs other privacy projects](https://www.notion.so/Privacy-projects-comparison-2688f96fb65c8096b694ecf7e4deca30) +- [NSSA state model](https://www.notion.so/Public-state-model-decision-2388f96fb65c80758b20c76de07b1fcc) +- [NSSA sequencer specs](https://www.notion.so/Sequencer-specs-2428f96fb65c802da2bfea7b0b214ecb) +- [NSSA sequencer code](https://www.notion.so/NSSA-sequencer-pseudocode-2508f96fb65c805e8859e047dffd6785) +- [NSSA Token program desing](https://www.notion.so/Token-program-design-2538f96fb65c80a1b4bdc4fd9dd162d7) +- [NSSA cross program calls](https://www.notion.so/NSSA-cross-program-calls-Tail-call-model-proposal-extended-version-2838f96fb65c8096b3a2d390444193b6) -For more details you can read [here](https://notes.status.im/Ya2wDpIyQquoiRiuEIM8hQ?view). # Install dependencies - Install build dependencies - On Linux ```sh @@ -31,3 +90,148 @@ Then restart your shell and run ```sh rzup install ``` + +# Run tests + +The NSSA repository includes both unit and integration test suites. + +### Unit tests + +```bash +# RISC0_DEV_MODE=1 is used to skip proof generation and reduce test runtime overhead +RISC0_DEV_MODE=1 cargo test --release +``` + +### Integration tests + +```bash +export NSSA_WALLET_HOME_DIR=$(pwd)/integration_tests/configs/debug/wallet/ +cd integration_tests +# RISC0_DEV_MODE=1 skips proof generation; RUST_LOG=info enables runtime logs +RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all +``` + +# Run the sequencer + +The sequencer can be run locally: + +```bash +cd sequencer_runner +RUST_LOG=info cargo run --release configs/debug +``` + +If everything went well you should see an output similar to this: +```bash +[2025-11-13T19:50:29Z INFO sequencer_runner] Sequencer core set up +[2025-11-13T19:50:29Z INFO network] Starting http server at 0.0.0.0:3040 +[2025-11-13T19:50:29Z INFO actix_server::builder] starting 8 workers +[2025-11-13T19:50:29Z INFO sequencer_runner] HTTP server started +[2025-11-13T19:50:29Z INFO sequencer_runner] Starting main sequencer loop +[2025-11-13T19:50:29Z INFO actix_server::server] Tokio runtime found; starting in existing Tokio runtime +[2025-11-13T19:50:29Z INFO actix_server::server] starting service: "actix-web-service-0.0.0.0:3040", workers: 8, listening on: 0.0.0.0:3040 +[2025-11-13T19:50:39Z INFO sequencer_runner] Collecting transactions from mempool, block creation +[2025-11-13T19:50:39Z INFO sequencer_core] Created block with 0 transactions in 0 seconds +[2025-11-13T19:50:39Z INFO sequencer_runner] Block with id 2 created +[2025-11-13T19:50:39Z INFO sequencer_runner] Waiting for new transactions +``` + +# Try the Wallet CLI + +## Install +This repo contains a CLI to interact with the Nescience sequencer. To install it run the following from the root directory of the repository. + +```bash +cargo install --path wallet --force +``` + +To use it the environment variable `NSSA_WALLET_HOME_DIR` needs to be set to the path where the wallet configuration file is. +There is one configuration file in `integration_tests/configs/debug/wallet/` that can be used. For that, from the root directory of this repository run: +```bash +export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ +``` + +## Tutorial + +### Health-check + +Check that the node is running and the wallet can connect to it with the following command + +```bash +wallet check-health +``` + +You should see `✅All looks good!`. + +### The commands + +The wallet comes with a variety of commands to interact and fetch information from the node. Run `wallet help` to see the available commands. + +```bash +Commands: + auth-transfer Authenticated transfer subcommand + chain-info Generic chain info subcommand + account Account view and sync subcommand + pinata Pinata program interaction subcommand + token Token program interaction subcommand + check-health Check the wallet can connect to the node and builtin local programs match the remote versions +``` + +### Accounts +Every piece of state in NSSA is encoded in an account. Public and private accounts can be created with the CLI. + +#### Create a new public account +```bash +wallet account new public + +# Output: +Generated new account with addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ +``` + +The address is the identifier of the account needed when executing programs that involve it. + +##### Account initialization +To see the current status of the newly generated account run + +```bash +# Replace the address with yours +wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ + +# Output: +Account is Uninitialized +``` + +Every new account is uninitialized. That means that it is not yet associated with any program. Programs can claim uninitialized accounts. Once a program claims an account, it will be owned by that program. This process is irreversible. + +How to do that depends on each program. In this section we'll initialize the account for the **Authenticated transfers program**. It is a program that safely handles native token transfers by requiring authentication to debit funds. + +To initialize the account under the ownership of the Authenticated transfer program run: + +```bash +# This command will submit a public transaction to execute the `init` function of +# the Authenticated-transfer program. The wallet will poll the sequencer to check +# that the transaction was accepted in a block. That may take some seconds. +wallet auth-transfer init --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ +``` + +Once that finishes, you can check the new status of the account with the same command as before + +```bash +wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ + +# Output: +Account owned by authenticated transfer program +{"balance":0} +``` + +#### Create a new private account +```bash +wallet account new private + +# Output: +Generated new account with addr Private/6n9d68Q3riGyWHbcGFLigmjaaE49bpGBpwq3TYbfgLNv +With npk 5b09bc16a637c7154a85d3cfce2c0152fadfcd36b38dcc00479aac3f3dd291fc +With ipk 02e12ecdabc33d207624823062e10e2d2c1246180431c476e816ffd9e634badf34 +``` + + + From e707b1b74f62d0ebfecccf8e62bafd90c3e35396 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 13 Nov 2025 21:14:30 -0300 Subject: [PATCH 12/90] add tutorial sections --- README.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 138 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index b7a13d2..a9c9688 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Nescience -Nescience State Separation Architecture (NSSA) is a programmable blockchain system that introduces a clean separation between public and private states, while keeping them fully interoperable. It lets developers build apps that can operate across both transparent and privacy-preserving accounts without changing how they write or deploy programs. Privacy is handled automatically by the protocol through zero-knowledge proofs (ZKPs). The result is a fully composable blockchain where privacy comes built-in. +Nescience State Separation Architecture (NSSA) is a programmable blockchain system that introduces a clean separation between public and private states, while keeping them fully interoperable. It lets developers build apps that can operate across both transparent and privacy-preserving accounts. Privacy is handled automatically by the protocol through zero-knowledge proofs (ZKPs). The result is a programmable blockchain where privacy comes built-in. ## Background @@ -138,33 +138,32 @@ If everything went well you should see an output similar to this: # Try the Wallet CLI ## Install -This repo contains a CLI to interact with the Nescience sequencer. To install it run the following from the root directory of the repository. +This repository includes a CLI for interacting with the Nescience sequencer. To install it, run the following command from the root of the repository: ```bash cargo install --path wallet --force ``` +Before using the CLI, set the environment variable `NSSA_WALLET_HOME_DIR` to the directory containing the wallet configuration file. A sample configuration is available at `integration_tests/configs/debug/wallet/`. To use it, run: -To use it the environment variable `NSSA_WALLET_HOME_DIR` needs to be set to the path where the wallet configuration file is. -There is one configuration file in `integration_tests/configs/debug/wallet/` that can be used. For that, from the root directory of this repository run: ```bash -export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ +export NSSA_WALLET_HOME_DIR=$(pwd)/integration_tests/configs/debug/wallet/ ``` ## Tutorial ### Health-check -Check that the node is running and the wallet can connect to it with the following command +Verify that the node is running and that the wallet can connect to it: ```bash wallet check-health ``` -You should see `✅All looks good!`. +You should see `✅ All looks good!`. ### The commands -The wallet comes with a variety of commands to interact and fetch information from the node. Run `wallet help` to see the available commands. +The wallet provides several commands to interact with the node and query state. To see the full list, run: ```bash Commands: @@ -177,7 +176,8 @@ Commands: ``` ### Accounts -Every piece of state in NSSA is encoded in an account. Public and private accounts can be created with the CLI. + +Every piece of state in NSSA is stored in an account. You can create both public and private accounts through the CLI. #### Create a new public account ```bash @@ -187,10 +187,11 @@ wallet account new public Generated new account with addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ ``` -The address is the identifier of the account needed when executing programs that involve it. +This address is required when executing any program that interacts with the account. -##### Account initialization -To see the current status of the newly generated account run +#### Account initialization + +To query the account’s current status, run: ```bash # Replace the address with yours @@ -200,20 +201,20 @@ wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ Account is Uninitialized ``` -Every new account is uninitialized. That means that it is not yet associated with any program. Programs can claim uninitialized accounts. Once a program claims an account, it will be owned by that program. This process is irreversible. +New accounts start as uninitialized, meaning no program owns them yet. Programs can claim uninitialized accounts; once claimed, the account becomes permanently owned by that program. -How to do that depends on each program. In this section we'll initialize the account for the **Authenticated transfers program**. It is a program that safely handles native token transfers by requiring authentication to debit funds. +In this example, we will initialize the account for the Authenticated transfer program, which securely manages native token transfers by requiring authentication for debits. -To initialize the account under the ownership of the Authenticated transfer program run: +Initialize the account by running: ```bash -# This command will submit a public transaction to execute the `init` function of -# the Authenticated-transfer program. The wallet will poll the sequencer to check -# that the transaction was accepted in a block. That may take some seconds. +# This command submits a public transaction executing the `init` function of the +# Authenticated-transfer program. The wallet polls the sequencer until the +# transaction is included in a block, which may take several seconds. wallet auth-transfer init --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ ``` -Once that finishes, you can check the new status of the account with the same command as before +After it completes, check the updated account status: ```bash wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ @@ -223,15 +224,130 @@ Account owned by authenticated transfer program {"balance":0} ``` +#### Funding the account: executing the Piñata program + +Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program. + +```bash +# Complete with your address and the correct solution for your case +# TODO: Explain how to find the solution +wallet pinata claim --to-addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ --solution 989106 +``` + +After the claim succeeds, the account will be funded with some tokens: + +```bash +wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ + +# Output: +Account owned by authenticated transfer program +{"balance":150} +``` + +#### Token transfer: executing the Authenticated transfers program + +The wallet CLI provides commands to execute the `Transfer` function of the authenticated program. Let's create another account for the recipient of the transfer. + +```bash +wallet account new public + +# Output: +Generated new account with addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS +``` + +The new account is uninitialized. The authenticated transfers program will claim any uninitialized account used in a transfer. So we don't need to manually initialize the recipient account. + +Let's send 37 tokens to the new account. + +```bash +wallet auth-transfer send \ + --from Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ \ + --to Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \ + --amount 37 +``` + +Once that succeeds we can check the states. + +```bash +# Sender account +wallet account get --addr Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8 + +# Output: +Account owned by authenticated transfer program +{"balance":113} +``` + +```bash +# Recipient account +wallet account get --addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS + +# Output: +Account owned by authenticated transfer program +{"balance":37} +``` + #### Create a new private account + +Now let’s switch to the private state and create a private account. + ```bash wallet account new private # Output: -Generated new account with addr Private/6n9d68Q3riGyWHbcGFLigmjaaE49bpGBpwq3TYbfgLNv -With npk 5b09bc16a637c7154a85d3cfce2c0152fadfcd36b38dcc00479aac3f3dd291fc -With ipk 02e12ecdabc33d207624823062e10e2d2c1246180431c476e816ffd9e634badf34 +Generated new account with addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL +With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951 +With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17 ``` +For now, focus only on the account address. Ignore the `npk` and `ipk` values. These are stored locally in the wallet and are used internally to build privacy-preserving transactions. We won't need them yet. +Just like public accounts, new private accounts start out uninitialized: + +```bash +wallet account get --addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL + +# Output: +Account is Uninitialized +``` +Unlike public accounts, private accounts are never visible to the network. They exist only in your local wallet storage. + +#### Sending tokens from the public account to the private account + +Sending tokens to an uninitialized private account causes the Authenticated-Transfers program to claim it. This happens because program execution logic does not depend on whether the involved accounts are public or private. + +Let’s send 17 tokens to the new private account. + +The syntax is identical to the public-to-public transfer; just set the private address as the recipient. + +This command will run the Authenticated-Transfer program locally, generate a proof, and submit it to the sequencer. Depending on your machine, this can take from 30 seconds to 4 minutes. + +```bash +wallet auth-transfer send \ + --from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \ + --to Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL \ + --amount 17 +``` + +After it succeeds, check both accounts: + +```bash +# Public sender account +wallet account get --addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS + +# Output: +Account owned by authenticated transfer program +{"balance":20} +``` + +```bash +# Private recipient account +wallet account get --addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL + +# Output: +Account owned by authenticated transfer program +{"balance":17} +``` + +Note: the last command does not query the network. +It works even offline because private account data lives only in your wallet storage. Other users cannot read your private balances. From ee47d98300676bf80f723504a8ae9bd46d8388d7 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 15 Nov 2025 01:28:00 -0300 Subject: [PATCH 13/90] add section token program --- README.md | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 200 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a9c9688..994db8c 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ Account owned by authenticated transfer program {"balance":0} ``` -#### Funding the account: executing the Piñata program +### Funding the account: executing the Piñata program Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program. @@ -244,7 +244,7 @@ Account owned by authenticated transfer program {"balance":150} ``` -#### Token transfer: executing the Authenticated transfers program +### Native token transfers: executing the Authenticated transfers program The wallet CLI provides commands to execute the `Transfer` function of the authenticated program. Let's create another account for the recipient of the transfer. @@ -299,7 +299,8 @@ With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951 With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17 ``` -For now, focus only on the account address. Ignore the `npk` and `ipk` values. These are stored locally in the wallet and are used internally to build privacy-preserving transactions. We won't need them yet. +For now, focus only on the account address. Ignore the `npk` and `ipk` values. These are stored locally in the wallet and are used internally to build privacy-preserving transactions. +Also, the account id for private accounts is derived from the `npk` and `ipk` values. But we won't need them now. Just like public accounts, new private accounts start out uninitialized: @@ -313,7 +314,8 @@ Unlike public accounts, private accounts are never visible to the network. They #### Sending tokens from the public account to the private account -Sending tokens to an uninitialized private account causes the Authenticated-Transfers program to claim it. This happens because program execution logic does not depend on whether the involved accounts are public or private. +Sending tokens to an uninitialized private account causes the Authenticated-Transfers program to claim it. Just like with public accounts. +This happens because program execution logic does not depend on whether the involved accounts are public or private. Let’s send 17 tokens to the new private account. @@ -351,3 +353,197 @@ Account owned by authenticated transfer program Note: the last command does not query the network. It works even offline because private account data lives only in your wallet storage. Other users cannot read your private balances. +#### Digression: modifying private accounts + +As a general rule, private accounts can only be modified through a program execution performed by their owner. That is, the person who holds the private key for that account. There is one exception: an uninitialized private account may be initialized by any user, without requiring the private key. After initialization, only the owner can modify it. + +This mechanism enables a common use case: transferring funds from any account (public or private) to a private account owned by someone else. For such transfers, the recipient’s private account must be uninitialized. + + +#### Sending tokens from the public account to a private account owned by someone else + +For this tutorial, we’ll simulate that scenario by creating a new private account that we own, but we’ll treat it as if it belonged to someone else. + +Let's create a new (uninitialized) private account like before: + +```bash +wallet account new private + +# Output: +Generated new account with addr Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5 +With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e +With ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 +``` + +Now we'll ignore the private account address and focus on the `npk` and `ipk` values. We'll need this to send tokens to a foreign private account. Syntax is very similar. + +```bash +wallet auth-transfer send \ + --from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \ + --to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \ + --to-ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \ + --amount 3 +``` + +The command above produces a privacy-preserving transaction, which may take a few minutes to complete. The updated values of the private account are encrypted and included in the transaction. + +Once the transaction is accepted, the recipient must run `wallet account sync-private`. This command scans the chain for encrypted values that belong to their private accounts and updates the local versions accordingly. + + +#### Transfers in other combinations of public and private accounts + +We’ve shown how to use the authenticated-transfers program for transfers between two public accounts, and for transfers from a public sender to a private recipient. Sending tokens from a private account (whether to a public account or to another private account) works in essentially the same way. + +### The token program + +So far, we’ve made transfers using the authenticated-transfers program, which handles native token transfers. The Token program, on the other hand, is used for creating and managing custom tokens. + +The Token program manages its accounts in two categories. Meaning, all accounts owned by the Token program fall into one of these types. +- Token definition accounts: these accounts store metadata about a token, such as its name, total supply, and other identifying properties. They act as the token’s unique identifier. +- Token holding accounts: these accounts hold actual token balances. In addition to the balance, they also record which token definition they belong to. + +#### Creating a new token + +To create a new token, simply run `wallet token new`. This will create a transaction to execute the `New` function of the token program. +The command expects a name, the desired total supply, and two uninitialized accounts: +- One that will be initialized as the token definition account for the new token. +- Another that will be initialized as a token holding account and receive the token’s entire initial supply. + + +##### New token with both definition and supply accounts set as public + +For example, let's create two new (uninitialized) public accounts and then use them to create a new token. + +```bash +wallet account new public + +# Output: +Generated new account with addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 +``` + +```bash +wallet account new public + +# Output: +Generated new account with addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw +``` + +Now we use them to create a new token. Let's call it the "Token A" + +```bash +wallet token new \ + --name TOKENA \ + --total-supply 1337 \ + --definition-addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 \ + --supply-addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw +``` + +After it succeeds, we can inspect the two accounts to see how they were initialized. + +```bash +wallet account get --addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 + +# Output: +Definition account owned by token program +{"account_type":"Token definition","name":"TOKENA","total_supply":1337} +``` + +```bash +wallet account get --addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw + +# Output: +Holding account owned by token program +{"account_type":"Token holding","definition_id":"4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7","balance":1337} +``` + +##### New token with both definition and supply accounts set as public + +Let’s create a new token, but this time using a public definition account and a private holding account to store the entire supply. + +Since we can’t reuse the accounts from the previous example, we need to create fresh ones for this case. + +```bash +wallet account new public + +# Output: +Generated new account with addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii +``` + +```bash +wallet account new private + + +# Output: +Generated new account with addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF +With npk 6a2dfe433cf28e525aa0196d719be3c16146f7ee358ca39595323f94fde38f93 +With ipk 03d59abf4bee974cc12ddb44641c19f0b5441fef39191f047c988c29a77252a577 +``` + +And we use them to create the token. + +Now we use them to create a new token. Let's call it "Token B". + +```bash +wallet token new \ + --name TOKENB \ + --total-supply 7331 \ + --definition-addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii \ + --supply-addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF +``` + +After it succeeds, we can check their values + + +```bash +wallet account get --addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii + +# Output: +Definition account owned by token program +{"account_type":"Token definition","name":"TOKENB","total_supply":7331} +``` + +```bash +wallet account get --addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF + +# Output: +Holding account owned by token program +{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":7331} +``` + +Like any other private account owned by us, it cannot be seen by other users. + +#### Custom token transfers + +The Token program has a function to move funds from one token holding account to another one. If executed with an uninitialized account as the recipient, this will be automatically claimed by the token program. + +The transfer function can be executed with the `wallet token send` command. + +Let's create a new public account for the recipient. + +```bash +wallet account new public + +# Output: +Generated new account with addr Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 +``` + +Let's send 10 B tokens to this new account. We'll debit this from the supply account used in the creation of the token. + +```bash +wallet token send \ + --from Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF \ + --to Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \ + --amount 10 +``` + +Let's inspect the public account: + +```bash +wallet account get --addr Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 + +# Output: +Holding account owned by token program +{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":10} +``` + + From 521ba5adbd21fc097526bf4a55f7d57580006606 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 15 Nov 2025 01:45:17 -0300 Subject: [PATCH 14/90] add commands --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 994db8c..d13fc64 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ You should see `✅ All looks good!`. ### The commands -The wallet provides several commands to interact with the node and query state. To see the full list, run: +The wallet provides several commands to interact with the node and query state. To see the full list, run `wallet help`: ```bash Commands: @@ -177,9 +177,19 @@ Commands: ### Accounts -Every piece of state in NSSA is stored in an account. You can create both public and private accounts through the CLI. +Every piece of state in NSSA is stored in an account. The CLI provides commands to manage accounts. Run `wallet account` to see the options available: +```bash +Commands: + get Get account data + new Produce new public or private account + sync-private Sync private accounts + help Print this message or the help of the given subcommand(s) +``` #### Create a new public account + +You can create both public and private accounts through the CLI. For example: + ```bash wallet account new public @@ -246,7 +256,16 @@ Account owned by authenticated transfer program ### Native token transfers: executing the Authenticated transfers program -The wallet CLI provides commands to execute the `Transfer` function of the authenticated program. Let's create another account for the recipient of the transfer. +NSSA comes with a program for managing and transferring native tokens. Run `wallet auth-transfer` to see the options available: +```bash +Commands: + init Initialize account under authenticated transfer program + send Send native tokens from one account to another with variable privacy + help Print this message or the help of the given subcommand(s) +``` + +We have already used the `init` command. The `send` command is used to execute the `Transfer` function of the authenticated program. +Let's try it. For that we need to create another account for the recipient of the transfer. ```bash wallet account new public @@ -397,6 +416,14 @@ We’ve shown how to use the authenticated-transfers program for transfers betwe ### The token program So far, we’ve made transfers using the authenticated-transfers program, which handles native token transfers. The Token program, on the other hand, is used for creating and managing custom tokens. +The CLI provides commands to execute the token program. To see the options available run `wallet token`: + +```bash +Commands: + new Produce a new token + send Send tokens from one account to another with variable privacy + help Print this message or the help of the given subcommand(s) +``` The Token program manages its accounts in two categories. Meaning, all accounts owned by the Token program fall into one of these types. - Token definition accounts: these accounts store metadata about a token, such as its name, total supply, and other identifying properties. They act as the token’s unique identifier. @@ -546,4 +573,23 @@ Holding account owned by token program {"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":10} ``` +### Chain information + +The wallet provides some commands to query information about the chain. These are under the `wallet chain-info` command. + +```bash +Commands: + current-block-id Get current block id from sequencer + block Get block at id from sequencer + transaction Get transaction at hash from sequencer +``` + +For example, run this to find the current block id. + +```bash +wallet chain-info current-block-id + +# Output: +Last block id is 65537 +``` From f2fb98608acc35daf53c82bf1c15ba9d17b29c13 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 15 Nov 2025 02:18:39 -0300 Subject: [PATCH 15/90] Add pinata program instructions --- README.md | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d13fc64..a092c77 100644 --- a/README.md +++ b/README.md @@ -236,11 +236,10 @@ Account owned by authenticated transfer program ### Funding the account: executing the Piñata program -Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program. +Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program. See the [Pinata](#piñata-program) section for instructions on how to use it. ```bash # Complete with your address and the correct solution for your case -# TODO: Explain how to find the solution wallet pinata claim --to-addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ --solution 989106 ``` @@ -573,6 +572,41 @@ Holding account owned by token program {"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":10} ``` +### Piñata program + +The testnet comes with a program that serves as a faucet for native tokens. We call it the Piñata. Use the command `wallet pinata claim` to get native tokens from it. This requires two parameters: +- `--to-addr` is the address of the account that will receive the tokens. **Use only initialized accounts here.** +- `--solution` a solution to the Pinata challenge. This will change every time the Pinata is successfully claimed. + +To find the solution to the challenge, first query the Pinata account. This is always at the address: `Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7`. + +```bash +wallet account get --addr Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7 + +# Output: +{"balance":750,"program_owner_b64":"/SQ9PX+NYQgXm7YMP7VMUBRwvU/Bq4pHTTZcCpTC5FM=","data_b64":"A939OBnG9OhvzOocqfCAJKSYvtcuV15OHDIVNg34MC8i","nonce":0} +``` + +Copy the `data_b64` value and run the following python script: + +```python +import base64, hashlib + +def find_16byte_prefix(data: str, max_attempts: int) -> bytes: + data = base64.b64decode(data_b64)[1:] + for attempt in range(max_attempts): + suffix = attempt.to_bytes(16, 'little') + h = hashlib.sha256(data + suffix).digest() + if h[:3] == b"\x00\x00\x00": + solution = int.from_bytes(suffix, 'little') + return f"Solution: {solution}" + raise RuntimeError(f"No suffix found in {max_attempts} attempts") + + +data_b64 = "A939OBnG9OhvzOocqfCAJKSYvtcuV15OHDIVNg34MC8i" # <- Change with the value from the Piñata account +print(find_16byte_prefix(data_b64, 50000000)) +``` + ### Chain information The wallet provides some commands to query information about the chain. These are under the `wallet chain-info` command. @@ -593,3 +627,4 @@ wallet chain-info current-block-id Last block id is 65537 ``` + From 0ad6d290ae3c8c352307e58c21d4f3635dfb86b6 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 15 Nov 2025 02:25:51 -0300 Subject: [PATCH 16/90] fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a092c77..e35eacb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ To the best of our knowledge, this approach is unique to Nescience. Other progra ### Example: creating and transferring tokens across states 1. Token creation (public execution): - - Alice submits a transaction to execute the token program `Create` function on-chain. + - Alice submits a transaction to execute the token program `New` function on-chain. - A new public token account is created, representing the token. - The minted tokens are recorded on-chain and fully visible on Alice's public account. 2. Transfer from public to private (local / privacy-preserving execution) @@ -482,7 +482,7 @@ Holding account owned by token program {"account_type":"Token holding","definition_id":"4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7","balance":1337} ``` -##### New token with both definition and supply accounts set as public +##### New token with public account definition but private holding account for initial supply Let’s create a new token, but this time using a public definition account and a private holding account to store the entire supply. From fd4ebde1fb76d33a4289d6959a2fb789fa3b4208 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 17 Nov 2025 15:43:01 -0300 Subject: [PATCH 17/90] fix account passing mechanism --- nssa/core/src/program.rs | 2 +- nssa/src/public_transaction/transaction.rs | 47 +++++++------------ nssa/src/state.rs | 26 +++++----- .../guest/src/bin/chain_caller.rs | 4 +- 4 files changed, 32 insertions(+), 47 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 8db2679..1a9119c 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -17,7 +17,7 @@ pub struct ProgramInput { pub struct ChainedCall { pub program_id: ProgramId, pub instruction_data: InstructionData, - pub account_indices: Vec, + pub pre_states: Vec, } #[derive(Serialize, Deserialize, Clone)] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 199d60d..c1eebd0 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -115,11 +115,22 @@ impl PublicTransaction { let mut program_output = program.execute(&input_pre_states, &instruction_data)?; - // This check is equivalent to checking that the program output pre_states coinicide - // with the values in the public state or with any modifications to those values - // during the chain of calls. - if input_pre_states != program_output.pre_states { - return Err(NssaError::InvalidProgramBehavior); + for pre in program_output.pre_states.iter() { + let account_id = pre.account_id; + // Check that the program output pre_states coinicide with the values in the public + // state or with any modifications to those values during the chain of calls. + let expected_pre = state_diff + .get(&account_id) + .cloned() + .unwrap_or_else(|| state.get_account_by_address(&account_id)); + if pre.account != expected_pre { + return Err(NssaError::InvalidProgramBehavior); + } + + // Check that authorization flags are consistent with the provided ones + if pre.is_authorized && !signer_addresses.contains(&account_id) { + return Err(NssaError::InvalidProgramBehavior); + } } // Verify execution corresponds to a well-behaved program. @@ -153,31 +164,7 @@ impl PublicTransaction { if let Some(next_chained_call) = chained_calls.pop() { program_id = next_chained_call.program_id; instruction_data = next_chained_call.instruction_data; - - // Build post states with metadata for next call - let mut post_states_with_metadata = Vec::new(); - for (pre, post) in program_output - .pre_states - .iter() - .zip(program_output.post_states) - { - let mut post_with_metadata = pre.clone(); - post_with_metadata.account = post.clone(); - post_states_with_metadata.push(post_with_metadata); - } - - input_pre_states = next_chained_call - .account_indices - .iter() - .map(|&i| { - post_states_with_metadata - .get(i) - .ok_or_else(|| { - NssaError::InvalidInput("Invalid account indices".into()) - }) - .cloned() - }) - .collect::, NssaError>>()?; + input_pre_states = next_chained_call.pre_states; } else { break; }; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index d53609c..3dbb2ec 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2085,27 +2085,26 @@ pub mod tests { fn test_chained_call_succeeds() { let program = Program::chain_caller(); let key = PrivateKey::try_new([1; 32]).unwrap(); - let address = Address::from(&PublicKey::new_from_private_key(&key)); + let from_address = Address::from(&PublicKey::new_from_private_key(&key)); + let to_address = Address::new([2; 32]); let initial_balance = 100; - let initial_data = [(address, initial_balance)]; + let initial_data = [(from_address, initial_balance), (to_address, 0)]; let mut state = V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); - let from = address; let from_key = key; - let to = Address::new([2; 32]); - let amount: u128 = 37; + let amount: u128 = 0; let instruction: (u128, ProgramId, u32) = (amount, Program::authenticated_transfer_program().id(), 2); let expected_to_post = Account { - program_owner: Program::chain_caller().id(), + program_owner: Program::authenticated_transfer_program().id(), balance: amount * 2, // The `chain_caller` chains the program twice ..Account::default() }; let message = public_transaction::Message::try_new( program.id(), - vec![to, from], //The chain_caller program permutes the account order in the chain call + vec![to_address, from_address], //The chain_caller program permutes the account order in the chain call vec![0], instruction, ) @@ -2115,8 +2114,8 @@ pub mod tests { state.transition_from_public_transaction(&tx).unwrap(); - let from_post = state.get_account_by_address(&from); - let to_post = state.get_account_by_address(&to); + let from_post = state.get_account_by_address(&from_address); + let to_post = state.get_account_by_address(&to_address); // The `chain_caller` program calls the program twice assert_eq!(from_post.balance, initial_balance - 2 * amount); assert_eq!(to_post, expected_to_post); @@ -2126,14 +2125,13 @@ pub mod tests { fn test_execution_fails_if_chained_calls_exceeds_depth() { let program = Program::chain_caller(); let key = PrivateKey::try_new([1; 32]).unwrap(); - let address = Address::from(&PublicKey::new_from_private_key(&key)); + let from_address = Address::from(&PublicKey::new_from_private_key(&key)); + let to_address = Address::new([2; 32]); let initial_balance = 100; - let initial_data = [(address, initial_balance)]; + let initial_data = [(from_address, initial_balance), (to_address, 0)]; let mut state = V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); - let from = address; let from_key = key; - let to = Address::new([2; 32]); let amount: u128 = 0; let instruction: (u128, ProgramId, u32) = ( amount, @@ -2143,7 +2141,7 @@ pub mod tests { let message = public_transaction::Message::try_new( program.id(), - vec![to, from], //The chain_caller program permutes the account order in the chain call + vec![to_address, from_address], //The chain_caller program permutes the account order in the chain call vec![0], instruction, ) diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index 5ebb6e6..028f8a0 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -24,7 +24,7 @@ fn main() { ChainedCall { program_id, instruction_data: instruction_data.clone(), - account_indices: vec![0, 1], + pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here }; num_chain_calls as usize - 1 ]; @@ -32,7 +32,7 @@ fn main() { chained_call.push(ChainedCall { program_id, instruction_data, - account_indices: vec![1, 0], // <- Account order permutation here + pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here }); write_nssa_outputs_with_chained_call( From ef73336aa573cacd98dc04058e0a504da724d9ec Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 18 Nov 2025 17:52:46 +0200 Subject: [PATCH 18/90] fix: borsh derivation on publci transactions --- nssa/core/Cargo.toml | 1 + nssa/core/src/address.rs | 3 +- nssa/program_methods/guest/Cargo.lock | 1 + nssa/src/encoding/public_transaction.rs | 144 +-------------------- nssa/src/public_transaction/message.rs | 3 +- nssa/src/public_transaction/transaction.rs | 3 +- nssa/src/public_transaction/witness_set.rs | 4 +- nssa/src/signature/mod.rs | 3 +- nssa/src/signature/public_key.rs | 3 +- nssa/test_program_methods/guest/Cargo.lock | 1 + 10 files changed, 20 insertions(+), 146 deletions(-) diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index 5712eaf..0e16a3f 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -12,6 +12,7 @@ chacha20 = { version = "0.9", default-features = false } k256 = { version = "0.13.3", optional = true } base58 = { version = "0.2.0", optional = true } anyhow = { version = "1.0.98", optional = true } +borsh = "1.5.7" [features] default = [] diff --git a/nssa/core/src/address.rs b/nssa/core/src/address.rs index 6355351..1d61e9e 100644 --- a/nssa/core/src/address.rs +++ b/nssa/core/src/address.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] @@ -6,7 +7,7 @@ use std::{fmt::Display, str::FromStr}; #[cfg(feature = "host")] use base58::{FromBase58, ToBase58}; -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize)] #[cfg_attr( any(feature = "host", test), derive(Debug, Copy, PartialOrd, Ord, Default) diff --git a/nssa/program_methods/guest/Cargo.lock b/nssa/program_methods/guest/Cargo.lock index 47585ba..563e8b9 100644 --- a/nssa/program_methods/guest/Cargo.lock +++ b/nssa/program_methods/guest/Cargo.lock @@ -1574,6 +1574,7 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" name = "nssa-core" version = "0.1.0" dependencies = [ + "borsh", "chacha20", "risc0-zkvm", "serde", diff --git a/nssa/src/encoding/public_transaction.rs b/nssa/src/encoding/public_transaction.rs index 03c34ee..5e6838c 100644 --- a/nssa/src/encoding/public_transaction.rs +++ b/nssa/src/encoding/public_transaction.rs @@ -1,153 +1,17 @@ -// TODO: Consider switching to deriving Borsh - -use std::io::{Cursor, Read}; - -use nssa_core::program::ProgramId; - -use crate::{ - Address, PublicKey, PublicTransaction, Signature, - error::NssaError, - public_transaction::{Message, WitnessSet}, -}; - -const MESSAGE_ENCODING_PREFIX_LEN: usize = 32; -const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = - b"/NSSA/v0.2/TxMessage/Public/\x00\x00\x00\x00"; +use crate::{PublicTransaction, error::NssaError, public_transaction::Message}; impl Message { - /// Serializes a `Message` into bytes in the following layout: - /// PREFIX || (4 bytes LE) * 8 || addresses_len (4 bytes LE) || addresses (32 bytes * N) || nonces_len (4 bytes LE) || nonces (16 bytes LE * M) || instruction_data_len || instruction_data (4 bytes LE * K) - /// Integers and words are encoded in little-endian byte order, and fields appear in the above order. pub(crate) fn to_bytes(&self) -> Vec { - let mut bytes = MESSAGE_ENCODING_PREFIX.to_vec(); - // program_id: [u32; 8] - for word in &self.program_id { - bytes.extend_from_slice(&word.to_le_bytes()); - } - // addresses: Vec<[u8;32]> - // serialize length as u32 little endian, then all addresses concatenated - let addresses_len = self.addresses.len() as u32; - bytes.extend(&addresses_len.to_le_bytes()); - for addr in &self.addresses { - bytes.extend_from_slice(addr.value()); - } - // nonces: Vec - // serialize length as u32 little endian, then all nonces concatenated in LE - let nonces_len = self.nonces.len() as u32; - bytes.extend(&nonces_len.to_le_bytes()); - for nonce in &self.nonces { - bytes.extend(&nonce.to_le_bytes()); - } - // instruction_data: Vec - // serialize length as u32 little endian, then all addresses concatenated - let instr_len = self.instruction_data.len() as u32; - bytes.extend(&instr_len.to_le_bytes()); - for word in &self.instruction_data { - bytes.extend(&word.to_le_bytes()); - } - - bytes - } - - pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let prefix = { - let mut this = [0u8; MESSAGE_ENCODING_PREFIX_LEN]; - cursor.read_exact(&mut this)?; - this - }; - if &prefix != MESSAGE_ENCODING_PREFIX { - return Err(NssaError::TransactionDeserializationError( - "Invalid public message prefix".to_string(), - )); - } - - let program_id: ProgramId = { - let mut this = [0u32; 8]; - for item in &mut this { - *item = u32_from_cursor(cursor)?; - } - this - }; - let addresses_len = u32_from_cursor(cursor)?; - let mut addresses = Vec::with_capacity(addresses_len as usize); - for _ in 0..addresses_len { - let mut value = [0u8; 32]; - cursor.read_exact(&mut value)?; - addresses.push(Address::new(value)) - } - let nonces_len = u32_from_cursor(cursor)?; - let mut nonces = Vec::with_capacity(nonces_len as usize); - for _ in 0..nonces_len { - let mut buf = [0u8; 16]; - cursor.read_exact(&mut buf)?; - nonces.push(u128::from_le_bytes(buf)) - } - let instruction_data_len = u32_from_cursor(cursor)?; - let mut instruction_data = Vec::with_capacity(instruction_data_len as usize); - for _ in 0..instruction_data_len { - let word = u32_from_cursor(cursor)?; - instruction_data.push(word) - } - Ok(Self { - program_id, - addresses, - nonces, - instruction_data, - }) - } -} - -impl WitnessSet { - pub(crate) fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - let size = self.signatures_and_public_keys().len() as u32; - bytes.extend_from_slice(&size.to_le_bytes()); - for (signature, public_key) in self.signatures_and_public_keys() { - bytes.extend_from_slice(signature.to_bytes()); - bytes.extend_from_slice(public_key.to_bytes()); - } - bytes - } - - pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let num_signatures: u32 = { - let mut buf = [0u8; 4]; - cursor.read_exact(&mut buf)?; - u32::from_le_bytes(buf) - }; - let mut signatures_and_public_keys = Vec::with_capacity(num_signatures as usize); - for _i in 0..num_signatures { - let signature = Signature::from_cursor(cursor)?; - let public_key = PublicKey::from_cursor(cursor)?; - signatures_and_public_keys.push((signature, public_key)) - } - Ok(Self { - signatures_and_public_keys, - }) + borsh::to_vec(&self).unwrap() } } impl PublicTransaction { pub fn to_bytes(&self) -> Vec { - let mut bytes = self.message().to_bytes(); - bytes.extend_from_slice(&self.witness_set().to_bytes()); - bytes + borsh::to_vec(&self).unwrap() } pub fn from_bytes(bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - Self::from_cursor(&mut cursor) - } - - pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let message = Message::from_cursor(cursor)?; - let witness_set = WitnessSet::from_cursor(cursor)?; - Ok(PublicTransaction::new(message, witness_set)) + Ok(borsh::from_slice(bytes)?) } } - -fn u32_from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let mut word_buf = [0u8; 4]; - cursor.read_exact(&mut word_buf)?; - Ok(u32::from_le_bytes(word_buf)) -} diff --git a/nssa/src/public_transaction/message.rs b/nssa/src/public_transaction/message.rs index 68cb5fb..86db4c7 100644 --- a/nssa/src/public_transaction/message.rs +++ b/nssa/src/public_transaction/message.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ account::Nonce, program::{InstructionData, ProgramId}, @@ -6,7 +7,7 @@ use serde::Serialize; use crate::{Address, error::NssaError, program::Program}; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Message { pub(crate) program_id: ProgramId, pub(crate) addresses: Vec
, diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index d118d0c..00dd2d3 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; +use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ account::{Account, AccountWithMetadata}, address::Address, @@ -13,7 +14,7 @@ use crate::{ public_transaction::{Message, WitnessSet}, }; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct PublicTransaction { message: Message, witness_set: WitnessSet, diff --git a/nssa/src/public_transaction/witness_set.rs b/nssa/src/public_transaction/witness_set.rs index e5095ba..359ed72 100644 --- a/nssa/src/public_transaction/witness_set.rs +++ b/nssa/src/public_transaction/witness_set.rs @@ -1,6 +1,8 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + use crate::{PrivateKey, PublicKey, Signature, public_transaction::Message}; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct WitnessSet { pub(crate) signatures_and_public_keys: Vec<(Signature, PublicKey)>, } diff --git a/nssa/src/signature/mod.rs b/nssa/src/signature/mod.rs index 2f18360..97c8117 100644 --- a/nssa/src/signature/mod.rs +++ b/nssa/src/signature/mod.rs @@ -2,12 +2,13 @@ mod encoding; mod private_key; mod public_key; +use borsh::{BorshDeserialize, BorshSerialize}; pub use private_key::PrivateKey; pub use public_key::PublicKey; use rand::{RngCore, rngs::OsRng}; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Signature { value: [u8; 64], } diff --git a/nssa/src/signature/public_key.rs b/nssa/src/signature/public_key.rs index dbd7d64..181ea97 100644 --- a/nssa/src/signature/public_key.rs +++ b/nssa/src/signature/public_key.rs @@ -1,10 +1,11 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::address::Address; use crate::{PrivateKey, error::NssaError}; use sha2::{Digest, Sha256}; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct PublicKey([u8; 32]); impl PublicKey { diff --git a/nssa/test_program_methods/guest/Cargo.lock b/nssa/test_program_methods/guest/Cargo.lock index d7e5b67..85f566c 100644 --- a/nssa/test_program_methods/guest/Cargo.lock +++ b/nssa/test_program_methods/guest/Cargo.lock @@ -1579,6 +1579,7 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" name = "nssa-core" version = "0.1.0" dependencies = [ + "borsh", "chacha20", "risc0-zkvm", "serde", From 30bcd20ac5cdd0cb5f7f7349be75af1145d1f0cd Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 19 Nov 2025 09:00:32 +0200 Subject: [PATCH 19/90] fix: privacy-preserving test --- nssa/core/src/account.rs | 3 ++- nssa/core/src/commitment.rs | 3 ++- nssa/core/src/encryption/mod.rs | 3 ++- nssa/core/src/encryption/shared_key_derivation.rs | 3 ++- nssa/core/src/nullifier.rs | 3 ++- nssa/src/encoding/privacy_preserving_transaction.rs | 13 ++----------- nssa/src/privacy_preserving_transaction/circuit.rs | 3 ++- nssa/src/privacy_preserving_transaction/message.rs | 5 +++-- .../privacy_preserving_transaction/transaction.rs | 3 ++- .../privacy_preserving_transaction/witness_set.rs | 4 +++- 10 files changed, 22 insertions(+), 21 deletions(-) diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index f5e38b3..58d2bec 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -1,11 +1,12 @@ use crate::{address::Address, program::ProgramId}; +use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; pub type Nonce = u128; pub type Data = Vec; /// Account to be used both in public and private contexts -#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct Account { pub program_owner: ProgramId, diff --git a/nssa/core/src/commitment.rs b/nssa/core/src/commitment.rs index c1f1cf7..5234417 100644 --- a/nssa/core/src/commitment.rs +++ b/nssa/core/src/commitment.rs @@ -1,9 +1,10 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::sha::{Impl, Sha256}; use serde::{Deserialize, Serialize}; use crate::{NullifierPublicKey, account::Account}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq, Hash))] pub struct Commitment(pub(super) [u8; 32]); diff --git a/nssa/core/src/encryption/mod.rs b/nssa/core/src/encryption/mod.rs index 280451b..8f0c6be 100644 --- a/nssa/core/src/encryption/mod.rs +++ b/nssa/core/src/encryption/mod.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use chacha20::{ ChaCha20, cipher::{KeyIvInit, StreamCipher}, @@ -20,7 +21,7 @@ pub struct SharedSecretKey(pub [u8; 32]); pub struct EncryptionScheme; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))] pub struct Ciphertext(pub(crate) Vec); diff --git a/nssa/core/src/encryption/shared_key_derivation.rs b/nssa/core/src/encryption/shared_key_derivation.rs index d40b63e..2a05c85 100644 --- a/nssa/core/src/encryption/shared_key_derivation.rs +++ b/nssa/core/src/encryption/shared_key_derivation.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use k256::{ @@ -10,7 +11,7 @@ use k256::{ use crate::{SharedSecretKey, encryption::Scalar}; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Secp256k1Point(pub Vec); impl Secp256k1Point { diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index a1bc38c..ec30700 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::sha::{Impl, Sha256}; use serde::{Deserialize, Serialize}; @@ -40,7 +41,7 @@ impl From<&NullifierSecretKey> for NullifierPublicKey { pub type NullifierSecretKey = [u8; 32]; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq, Hash))] pub struct Nullifier(pub(super) [u8; 32]); diff --git a/nssa/src/encoding/privacy_preserving_transaction.rs b/nssa/src/encoding/privacy_preserving_transaction.rs index 5788e6f..a488e7b 100644 --- a/nssa/src/encoding/privacy_preserving_transaction.rs +++ b/nssa/src/encoding/privacy_preserving_transaction.rs @@ -209,20 +209,11 @@ impl WitnessSet { impl PrivacyPreservingTransaction { pub fn to_bytes(&self) -> Vec { - let mut bytes = self.message().to_bytes(); - bytes.extend_from_slice(&self.witness_set().to_bytes()); - bytes + borsh::to_vec(&self).unwrap() } pub fn from_bytes(bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - Self::from_cursor(&mut cursor) - } - - pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let message = Message::from_cursor(cursor)?; - let witness_set = WitnessSet::from_cursor(cursor)?; - Ok(PrivacyPreservingTransaction::new(message, witness_set)) + Ok(borsh::from_slice(bytes)?) } } diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 9ce0610..0962ee3 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, @@ -11,7 +12,7 @@ use crate::{error::NssaError, program::Program}; use crate::program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}; /// Proof of the privacy preserving execution circuit -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Proof(pub(crate) Vec); /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 5911838..10244a1 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -1,3 +1,4 @@ +use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput, account::{Account, Nonce}, @@ -9,7 +10,7 @@ use crate::{Address, error::NssaError}; pub type ViewTag = u8; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct EncryptedAccountData { pub ciphertext: Ciphertext, pub epk: EphemeralPublicKey, @@ -42,7 +43,7 @@ impl EncryptedAccountData { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Message { pub(crate) public_addresses: Vec
, pub(crate) nonces: Vec, diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index 3e89ba7..9de6641 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; +use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, account::{Account, AccountWithMetadata}, @@ -13,7 +14,7 @@ use crate::{Address, V02State}; use super::message::Message; use super::witness_set::WitnessSet; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct PrivacyPreservingTransaction { pub message: Message, witness_set: WitnessSet, diff --git a/nssa/src/privacy_preserving_transaction/witness_set.rs b/nssa/src/privacy_preserving_transaction/witness_set.rs index 9fc587e..b38b0fb 100644 --- a/nssa/src/privacy_preserving_transaction/witness_set.rs +++ b/nssa/src/privacy_preserving_transaction/witness_set.rs @@ -1,9 +1,11 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + use crate::{ PrivateKey, PublicKey, Signature, privacy_preserving_transaction::{circuit::Proof, message::Message}, }; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct WitnessSet { pub(crate) signatures_and_public_keys: Vec<(Signature, PublicKey)>, pub(crate) proof: Proof, From 2f0785397506601812bd718b6cc4c41ebcce46cc Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 19 Nov 2025 14:28:18 +0200 Subject: [PATCH 20/90] fix: borsh integrated into transactions --- nssa/core/src/account.rs | 4 +- .../privacy_preserving_transaction.rs | 237 +----------------- .../program_deployment_transaction.rs | 71 +----- .../privacy_preserving_transaction/message.rs | 13 - .../program_deployment_transaction/message.rs | 4 +- .../transaction.rs | 4 +- nssa/src/signature/encoding.rs | 27 -- nssa/src/signature/mod.rs | 1 - nssa/src/signature/public_key.rs | 2 +- 9 files changed, 21 insertions(+), 342 deletions(-) delete mode 100644 nssa/src/signature/encoding.rs diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 58d2bec..5b9995a 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -6,7 +6,9 @@ pub type Nonce = u128; pub type Data = Vec; /// Account to be used both in public and private contexts -#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive( + Serialize, Deserialize, Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, +)] #[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct Account { pub program_owner: ProgramId, diff --git a/nssa/src/encoding/privacy_preserving_transaction.rs b/nssa/src/encoding/privacy_preserving_transaction.rs index a488e7b..8647a32 100644 --- a/nssa/src/encoding/privacy_preserving_transaction.rs +++ b/nssa/src/encoding/privacy_preserving_transaction.rs @@ -1,209 +1,15 @@ -use std::io::{Cursor, Read}; - -use nssa_core::{ - Commitment, Nullifier, - account::Account, - encryption::{Ciphertext, EphemeralPublicKey}, -}; - use crate::{ - Address, PrivacyPreservingTransaction, PublicKey, Signature, - error::NssaError, - privacy_preserving_transaction::{ - circuit::Proof, - message::{EncryptedAccountData, Message}, - witness_set::WitnessSet, - }, + PrivacyPreservingTransaction, error::NssaError, + privacy_preserving_transaction::message::Message, }; -const MESSAGE_ENCODING_PREFIX_LEN: usize = 32; -const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = - b"/NSSA/v0.2/TxMessage/Private/\x00\x00\x00"; - -impl EncryptedAccountData { - pub fn to_bytes(&self) -> Vec { - let mut bytes = self.ciphertext.to_bytes(); - bytes.extend_from_slice(&self.epk.to_bytes()); - bytes.push(self.view_tag); - bytes - } - - pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let ciphertext = Ciphertext::from_cursor(cursor)?; - let epk = EphemeralPublicKey::from_cursor(cursor)?; - - let mut tag_bytes = [0; 1]; - cursor.read_exact(&mut tag_bytes)?; - let view_tag = tag_bytes[0]; - - Ok(Self { - ciphertext, - epk, - view_tag, - }) - } -} - impl Message { - pub(crate) fn to_bytes(&self) -> Vec { - let mut bytes = MESSAGE_ENCODING_PREFIX.to_vec(); - - // Public addresses - let public_addresses_len: u32 = self.public_addresses.len() as u32; - bytes.extend_from_slice(&public_addresses_len.to_le_bytes()); - for address in &self.public_addresses { - bytes.extend_from_slice(address.value()); - } - // Nonces - let nonces_len = self.nonces.len() as u32; - bytes.extend(&nonces_len.to_le_bytes()); - for nonce in &self.nonces { - bytes.extend(&nonce.to_le_bytes()); - } - // Public post states - let public_post_states_len: u32 = self.public_post_states.len() as u32; - bytes.extend_from_slice(&public_post_states_len.to_le_bytes()); - for account in &self.public_post_states { - bytes.extend_from_slice(&account.to_bytes()); - } - - // Encrypted post states - let encrypted_accounts_post_states_len: u32 = - self.encrypted_private_post_states.len() as u32; - bytes.extend_from_slice(&encrypted_accounts_post_states_len.to_le_bytes()); - for encrypted_account in &self.encrypted_private_post_states { - bytes.extend_from_slice(&encrypted_account.to_bytes()); - } - - // New commitments - let new_commitments_len: u32 = self.new_commitments.len() as u32; - bytes.extend_from_slice(&new_commitments_len.to_le_bytes()); - for commitment in &self.new_commitments { - bytes.extend_from_slice(&commitment.to_byte_array()); - } - - // New nullifiers - let new_nullifiers_len: u32 = self.new_nullifiers.len() as u32; - bytes.extend_from_slice(&new_nullifiers_len.to_le_bytes()); - for (nullifier, commitment_set_digest) in &self.new_nullifiers { - bytes.extend_from_slice(&nullifier.to_byte_array()); - bytes.extend_from_slice(commitment_set_digest); - } - - bytes + pub fn to_bytes(&self) -> Vec { + borsh::to_vec(&self).unwrap() } - #[allow(unused)] - pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let prefix = { - let mut this = [0u8; MESSAGE_ENCODING_PREFIX_LEN]; - cursor.read_exact(&mut this)?; - this - }; - if &prefix != MESSAGE_ENCODING_PREFIX { - return Err(NssaError::TransactionDeserializationError( - "Invalid privacy preserving message prefix".to_string(), - )); - } - - let mut len_bytes = [0u8; 4]; - - // Public addresses - cursor.read_exact(&mut len_bytes)?; - let public_addresses_len = u32::from_le_bytes(len_bytes) as usize; - let mut public_addresses = Vec::with_capacity(public_addresses_len); - for _ in 0..public_addresses_len { - let mut value = [0u8; 32]; - cursor.read_exact(&mut value)?; - public_addresses.push(Address::new(value)) - } - - // Nonces - cursor.read_exact(&mut len_bytes)?; - let nonces_len = u32::from_le_bytes(len_bytes) as usize; - let mut nonces = Vec::with_capacity(nonces_len); - for _ in 0..nonces_len { - let mut buf = [0u8; 16]; - cursor.read_exact(&mut buf)?; - nonces.push(u128::from_le_bytes(buf)) - } - - // Public post states - cursor.read_exact(&mut len_bytes)?; - let public_post_states_len = u32::from_le_bytes(len_bytes) as usize; - let mut public_post_states = Vec::with_capacity(public_post_states_len); - for _ in 0..public_post_states_len { - public_post_states.push(Account::from_cursor(cursor)?); - } - - // Encrypted private post states - cursor.read_exact(&mut len_bytes)?; - let encrypted_len = u32::from_le_bytes(len_bytes) as usize; - let mut encrypted_private_post_states = Vec::with_capacity(encrypted_len); - for _ in 0..encrypted_len { - encrypted_private_post_states.push(EncryptedAccountData::from_cursor(cursor)?); - } - - // New commitments - cursor.read_exact(&mut len_bytes)?; - let new_commitments_len = u32::from_le_bytes(len_bytes) as usize; - let mut new_commitments = Vec::with_capacity(new_commitments_len); - for _ in 0..new_commitments_len { - new_commitments.push(Commitment::from_cursor(cursor)?); - } - - // New nullifiers - cursor.read_exact(&mut len_bytes)?; - let new_nullifiers_len = u32::from_le_bytes(len_bytes) as usize; - let mut new_nullifiers = Vec::with_capacity(new_nullifiers_len); - for _ in 0..new_nullifiers_len { - let nullifier = Nullifier::from_cursor(cursor)?; - let mut commitment_set_digest = [0; 32]; - cursor.read_exact(&mut commitment_set_digest)?; - new_nullifiers.push((nullifier, commitment_set_digest)); - } - - Ok(Self { - public_addresses, - nonces, - public_post_states, - encrypted_private_post_states, - new_commitments, - new_nullifiers, - }) - } -} - -impl WitnessSet { - pub(crate) fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - let size = self.signatures_and_public_keys().len() as u32; - bytes.extend_from_slice(&size.to_le_bytes()); - for (signature, public_key) in self.signatures_and_public_keys() { - bytes.extend_from_slice(signature.to_bytes()); - bytes.extend_from_slice(public_key.to_bytes()); - } - bytes.extend_from_slice(&self.proof.to_bytes()); - bytes - } - - pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let num_signatures: u32 = { - let mut buf = [0u8; 4]; - cursor.read_exact(&mut buf)?; - u32::from_le_bytes(buf) - }; - let mut signatures_and_public_keys = Vec::with_capacity(num_signatures as usize); - for _i in 0..num_signatures { - let signature = Signature::from_cursor(cursor)?; - let public_key = PublicKey::from_cursor(cursor)?; - signatures_and_public_keys.push((signature, public_key)) - } - let proof = Proof::from_cursor(cursor)?; - Ok(Self { - signatures_and_public_keys, - proof, - }) + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(borsh::from_slice(bytes)?) } } @@ -216,34 +22,3 @@ impl PrivacyPreservingTransaction { Ok(borsh::from_slice(bytes)?) } } - -impl Proof { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - let proof_len = self.0.len() as u32; - bytes.extend_from_slice(&proof_len.to_le_bytes()); - bytes.extend_from_slice(&self.0); - bytes - } - - pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let proof_len = u32_from_cursor(cursor) as usize; - let mut proof = Vec::with_capacity(proof_len); - - for _ in 0..proof_len { - let mut one_byte_buf = [0u8]; - - cursor.read_exact(&mut one_byte_buf)?; - - proof.push(one_byte_buf[0]); - } - Ok(Self(proof)) - } -} - -// TODO: Improve error handling. Remove unwraps. -pub fn u32_from_cursor(cursor: &mut Cursor<&[u8]>) -> u32 { - let mut word_buf = [0u8; 4]; - cursor.read_exact(&mut word_buf).unwrap(); - u32::from_le_bytes(word_buf) -} diff --git a/nssa/src/encoding/program_deployment_transaction.rs b/nssa/src/encoding/program_deployment_transaction.rs index 2dc91b4..0cf00e0 100644 --- a/nssa/src/encoding/program_deployment_transaction.rs +++ b/nssa/src/encoding/program_deployment_transaction.rs @@ -1,77 +1,17 @@ -// TODO: Consider switching to deriving Borsh - -use std::io::{Cursor, Read}; - -use crate::{ - ProgramDeploymentTransaction, error::NssaError, program_deployment_transaction::Message, -}; - -const MESSAGE_ENCODING_PREFIX_LEN: usize = 32; -const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] = - b"/NSSA/v0.2/TxMessage/Program/\x00\x00\x00"; - -impl Message { - /// Serializes a `Message` into bytes in the following layout: - /// PREFIX || bytecode_len (4 bytes LE) || - /// Integers are encoded in little-endian byte order, and fields appear in the above order. - pub(crate) fn to_bytes(&self) -> Vec { - let mut bytes = MESSAGE_ENCODING_PREFIX.to_vec(); - let bytecode_len = self.bytecode.len() as u32; - bytes.extend(&bytecode_len.to_le_bytes()); - bytes.extend(&self.bytecode); - bytes - } - - pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let prefix = { - let mut this = [0u8; MESSAGE_ENCODING_PREFIX_LEN]; - cursor.read_exact(&mut this)?; - this - }; - if &prefix != MESSAGE_ENCODING_PREFIX { - return Err(NssaError::TransactionDeserializationError( - "Invalid public message prefix".to_string(), - )); - } - let bytecode_len = u32_from_cursor(cursor)?; - let mut bytecode = vec![0; bytecode_len as usize]; - let num_bytes = cursor.read(&mut bytecode)?; - if num_bytes != bytecode_len as usize { - println!("num bytes: {}", num_bytes); - return Err(NssaError::TransactionDeserializationError( - "Invalid number of bytes".to_string(), - )); - } - Ok(Self { bytecode }) - } -} +use crate::{ProgramDeploymentTransaction, error::NssaError}; impl ProgramDeploymentTransaction { pub fn to_bytes(&self) -> Vec { - self.message.to_bytes() + borsh::to_vec(&self).unwrap() } pub fn from_bytes(bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - Self::from_cursor(&mut cursor) + Ok(borsh::from_slice(bytes)?) } - - pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let message = Message::from_cursor(cursor)?; - Ok(Self::new(message)) - } -} - -fn u32_from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let mut word_buf = [0u8; 4]; - cursor.read_exact(&mut word_buf)?; - Ok(u32::from_le_bytes(word_buf)) } #[cfg(test)] mod tests { - use std::io::Cursor; - use crate::{ProgramDeploymentTransaction, program_deployment_transaction::Message}; #[test] @@ -79,8 +19,7 @@ mod tests { let message = Message::new(vec![0xca, 0xfe, 0xca, 0xfe, 0x01, 0x02, 0x03]); let tx = ProgramDeploymentTransaction::new(message); let bytes = tx.to_bytes(); - let mut cursor = Cursor::new(bytes.as_ref()); - let tx_from_cursor = ProgramDeploymentTransaction::from_cursor(&mut cursor).unwrap(); - assert_eq!(tx, tx_from_cursor); + let tx_from_bytes = ProgramDeploymentTransaction::from_bytes(&bytes).unwrap(); + assert_eq!(tx, tx_from_bytes); } } diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 10244a1..626e03e 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -91,8 +91,6 @@ impl Message { #[cfg(test)] pub mod tests { - use std::io::Cursor; - use nssa_core::{ Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey, account::Account, @@ -141,17 +139,6 @@ pub mod tests { } } - #[test] - fn test_message_serialization_roundtrip() { - let message = message_for_tests(); - - let bytes = message.to_bytes(); - let mut cursor = Cursor::new(bytes.as_ref()); - let message_from_cursor = Message::from_cursor(&mut cursor).unwrap(); - - assert_eq!(message, message_from_cursor); - } - #[test] fn test_encrypted_account_data_constructor() { let npk = NullifierPublicKey::from(&[1; 32]); diff --git a/nssa/src/program_deployment_transaction/message.rs b/nssa/src/program_deployment_transaction/message.rs index 6a5c670..65e9ec2 100644 --- a/nssa/src/program_deployment_transaction/message.rs +++ b/nssa/src/program_deployment_transaction/message.rs @@ -1,4 +1,6 @@ -#[derive(Debug, Clone, PartialEq, Eq)] +use borsh::{BorshDeserialize, BorshSerialize}; + +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Message { pub(crate) bytecode: Vec, } diff --git a/nssa/src/program_deployment_transaction/transaction.rs b/nssa/src/program_deployment_transaction/transaction.rs index 4ec2e10..c5f31a1 100644 --- a/nssa/src/program_deployment_transaction/transaction.rs +++ b/nssa/src/program_deployment_transaction/transaction.rs @@ -1,8 +1,10 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + use crate::{ V02State, error::NssaError, program::Program, program_deployment_transaction::message::Message, }; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct ProgramDeploymentTransaction { pub(crate) message: Message, } diff --git a/nssa/src/signature/encoding.rs b/nssa/src/signature/encoding.rs deleted file mode 100644 index 999e4a1..0000000 --- a/nssa/src/signature/encoding.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::io::{Cursor, Read}; - -use crate::{PublicKey, Signature, error::NssaError}; - -impl PublicKey { - pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let mut value = [0u8; 32]; - cursor.read_exact(&mut value)?; - Self::try_new(value) - } - - pub(crate) fn to_bytes(&self) -> &[u8] { - self.value() - } -} - -impl Signature { - pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let mut value = [0u8; 64]; - cursor.read_exact(&mut value)?; - Ok(Self { value }) - } - - pub(crate) fn to_bytes(&self) -> &[u8] { - &self.value - } -} diff --git a/nssa/src/signature/mod.rs b/nssa/src/signature/mod.rs index 97c8117..1081d9c 100644 --- a/nssa/src/signature/mod.rs +++ b/nssa/src/signature/mod.rs @@ -1,4 +1,3 @@ -mod encoding; mod private_key; mod public_key; diff --git a/nssa/src/signature/public_key.rs b/nssa/src/signature/public_key.rs index 181ea97..d7f8d2e 100644 --- a/nssa/src/signature/public_key.rs +++ b/nssa/src/signature/public_key.rs @@ -20,7 +20,7 @@ impl PublicKey { Self(value) } - pub(super) fn try_new(value: [u8; 32]) -> Result { + pub fn try_new(value: [u8; 32]) -> Result { // Check point is valid let _ = secp256k1::XOnlyPublicKey::from_byte_array(value) .map_err(|_| NssaError::InvalidPublicKey)?; From 8839edc377343ae88b21b48143f343749c8e9079 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 21 Nov 2025 08:27:16 +0200 Subject: [PATCH 21/90] fix: ci test 2 --- ci_scripts/lint-ubuntu.sh | 6 +++--- ci_scripts/test-ubuntu.sh | 14 +++++++------- integration_tests/src/test_suite_map.rs | 5 +++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ci_scripts/lint-ubuntu.sh b/ci_scripts/lint-ubuntu.sh index 16d46a2..9e8eafb 100755 --- a/ci_scripts/lint-ubuntu.sh +++ b/ci_scripts/lint-ubuntu.sh @@ -1,8 +1,8 @@ set -e -cargo install taplo-cli --locked +#cargo install taplo-cli --locked cargo fmt -- --check -taplo fmt --check +#taplo fmt --check -RISC0_SKIP_BUILD=1 cargo clippy --workspace --all-targets -- -D warnings +#RISC0_SKIP_BUILD=1 cargo clippy --workspace --all-targets -- -D warnings diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh index 8114cb7..d968104 100755 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -3,15 +3,15 @@ set -e curl -L https://risczero.com/install | bash /home/runner/.risc0/bin/rzup install -RISC0_DEV_MODE=1 cargo test --release --features no_docker +#RISC0_DEV_MODE=1 cargo test --release --features no_docker cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info -echo "Try test valid proof at least once" -cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account -echo "Continuing in dev mode" -RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all -cd .. +#echo "Try test valid proof at least once" +#cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account +#echo "Continuing in dev mode" +RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account_cont_run_path +#cd .. -cd nssa/program_methods/guest && cargo test --release +#cd nssa/program_methods/guest && cargo test --release diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index affefc1..10c7be2 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1189,6 +1189,11 @@ pub fn prepare_function_map() -> HashMap { .await .unwrap(); + info!("All private accounts data"); + for (addr, (_, acc)) in &wallet_storage.storage.user_data.user_private_accounts { + info!("{addr} :: {acc:#?}"); + } + assert_eq!(tx.message.new_commitments.len(), 2); for commitment in tx.message.new_commitments.into_iter() { assert!(verify_commitment_is_in_state(commitment, &seq_client).await); From effbfac7d9b318a54bf5782e2490e72fb349b19a Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 21 Nov 2025 08:49:56 +0200 Subject: [PATCH 22/90] fix: ci test 3 --- integration_tests/src/test_suite_map.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 10c7be2..bf6da53 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1185,6 +1185,7 @@ pub fn prepare_function_map() -> HashMap { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + let wallet_config = fetch_config().await.unwrap(); let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) .await .unwrap(); From 05e01f3a76b797dd34e2854d2cee4bc79aa3bf19 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 21 Nov 2025 09:06:30 +0200 Subject: [PATCH 23/90] fix: ci test 4 --- integration_tests/src/test_suite_map.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index bf6da53..f51874a 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1195,6 +1195,11 @@ pub fn prepare_function_map() -> HashMap { info!("{addr} :: {acc:#?}"); } + let new_commitment1 = wallet_storage + .get_private_account_commitment(&from) + .unwrap(); + assert_eq!(tx.message.new_commitments[0], new_commitment1); + assert_eq!(tx.message.new_commitments.len(), 2); for commitment in tx.message.new_commitments.into_iter() { assert!(verify_commitment_is_in_state(commitment, &seq_client).await); From dc57f6b02763171b5480c051846ef59e15acdcf2 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 21 Nov 2025 09:22:23 +0200 Subject: [PATCH 24/90] fix: ci test 5 --- ci_scripts/lint-ubuntu.sh | 6 +++--- ci_scripts/test-ubuntu.sh | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ci_scripts/lint-ubuntu.sh b/ci_scripts/lint-ubuntu.sh index 9e8eafb..16d46a2 100755 --- a/ci_scripts/lint-ubuntu.sh +++ b/ci_scripts/lint-ubuntu.sh @@ -1,8 +1,8 @@ set -e -#cargo install taplo-cli --locked +cargo install taplo-cli --locked cargo fmt -- --check -#taplo fmt --check +taplo fmt --check -#RISC0_SKIP_BUILD=1 cargo clippy --workspace --all-targets -- -D warnings +RISC0_SKIP_BUILD=1 cargo clippy --workspace --all-targets -- -D warnings diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh index d968104..af57bc7 100755 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -3,15 +3,15 @@ set -e curl -L https://risczero.com/install | bash /home/runner/.risc0/bin/rzup install -#RISC0_DEV_MODE=1 cargo test --release --features no_docker +RISC0_DEV_MODE=1 cargo test --release --features no_docker cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info -#echo "Try test valid proof at least once" -#cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account -#echo "Continuing in dev mode" +echo "Try test valid proof at least once" +cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account +echo "Continuing in dev mode" RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account_cont_run_path -#cd .. +cd .. -#cd nssa/program_methods/guest && cargo test --release +cd nssa/program_methods/guest && cargo test --release From 8af6365c5d96dc8216580cde3b33998fe86d0d0b Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 24 Nov 2025 08:47:33 -0300 Subject: [PATCH 25/90] noop --- 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 0bbc8b0..1b0f14c 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1345,7 +1345,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_pinata() { - info!("########## test_pinata ##########"); + info!("########## test_pinata ###########"); let pinata_addr = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; From adcf70da7613196e125ec835ea9e58fb99ff7d18 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 25 Nov 2025 08:58:15 +0200 Subject: [PATCH 26/90] fix: test run --- ci_scripts/test-ubuntu.sh | 2 +- nssa/core/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh index af57bc7..8114cb7 100755 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -11,7 +11,7 @@ export RUST_LOG=info echo "Try test valid proof at least once" cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account echo "Continuing in dev mode" -RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account_cont_run_path +RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all cd .. cd nssa/program_methods/guest && cargo test --release diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index 0e16a3f..67f40b2 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -12,7 +12,7 @@ chacha20 = { version = "0.9", default-features = false } k256 = { version = "0.13.3", optional = true } base58 = { version = "0.2.0", optional = true } anyhow = { version = "1.0.98", optional = true } -borsh = "1.5.7" +borsh.workspace = true [features] default = [] From 46fad3c5e7e27568d4a9e27ffbedd32c46fee191 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 25 Nov 2025 09:40:46 +0200 Subject: [PATCH 27/90] fix: comments fix 1 --- .../privacy_preserving_transaction.rs | 4 +-- .../program_deployment_transaction.rs | 2 +- nssa/src/encoding/public_transaction.rs | 4 +-- nssa/src/signature/public_key.rs | 26 ++++++++++++++++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/nssa/src/encoding/privacy_preserving_transaction.rs b/nssa/src/encoding/privacy_preserving_transaction.rs index 8647a32..fcb6c94 100644 --- a/nssa/src/encoding/privacy_preserving_transaction.rs +++ b/nssa/src/encoding/privacy_preserving_transaction.rs @@ -5,7 +5,7 @@ use crate::{ impl Message { pub fn to_bytes(&self) -> Vec { - borsh::to_vec(&self).unwrap() + borsh::to_vec(&self).expect("Autoderived borsh serialization failure") } pub fn from_bytes(bytes: &[u8]) -> Result { @@ -15,7 +15,7 @@ impl Message { impl PrivacyPreservingTransaction { pub fn to_bytes(&self) -> Vec { - borsh::to_vec(&self).unwrap() + borsh::to_vec(&self).expect("Autoderived borsh serialization failure") } pub fn from_bytes(bytes: &[u8]) -> Result { diff --git a/nssa/src/encoding/program_deployment_transaction.rs b/nssa/src/encoding/program_deployment_transaction.rs index 0cf00e0..ee66863 100644 --- a/nssa/src/encoding/program_deployment_transaction.rs +++ b/nssa/src/encoding/program_deployment_transaction.rs @@ -2,7 +2,7 @@ use crate::{ProgramDeploymentTransaction, error::NssaError}; impl ProgramDeploymentTransaction { pub fn to_bytes(&self) -> Vec { - borsh::to_vec(&self).unwrap() + borsh::to_vec(&self).expect("Autoderived borsh serialization failure") } pub fn from_bytes(bytes: &[u8]) -> Result { diff --git a/nssa/src/encoding/public_transaction.rs b/nssa/src/encoding/public_transaction.rs index 5e6838c..ea0988c 100644 --- a/nssa/src/encoding/public_transaction.rs +++ b/nssa/src/encoding/public_transaction.rs @@ -2,13 +2,13 @@ use crate::{PublicTransaction, error::NssaError, public_transaction::Message}; impl Message { pub(crate) fn to_bytes(&self) -> Vec { - borsh::to_vec(&self).unwrap() + borsh::to_vec(&self).expect("Autoderived borsh serialization failure") } } impl PublicTransaction { pub fn to_bytes(&self) -> Vec { - borsh::to_vec(&self).unwrap() + borsh::to_vec(&self).expect("Autoderived borsh serialization failure") } pub fn from_bytes(bytes: &[u8]) -> Result { diff --git a/nssa/src/signature/public_key.rs b/nssa/src/signature/public_key.rs index d7f8d2e..4e78f3b 100644 --- a/nssa/src/signature/public_key.rs +++ b/nssa/src/signature/public_key.rs @@ -5,9 +5,23 @@ use crate::{PrivateKey, error::NssaError}; use sha2::{Digest, Sha256}; -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize)] pub struct PublicKey([u8; 32]); +impl BorshDeserialize for PublicKey { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let mut buf = [0u8; 32]; + reader.read_exact(&mut buf)?; + + Self::try_new(buf).map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid public key: not a valid point", + ) + }) + } +} + impl PublicKey { pub fn new_from_private_key(key: &PrivateKey) -> Self { let value = { @@ -94,4 +108,14 @@ mod test { ); } } + + #[test] + fn test_correct_ser_deser_roundtrip() { + let pub_key = PublicKey::try_new([42; 32]).unwrap(); + + let pub_key_borsh_ser = borsh::to_vec(&pub_key).unwrap(); + let pub_key_new: PublicKey = borsh::from_slice(&pub_key_borsh_ser).unwrap(); + + assert_eq!(pub_key, pub_key_new); + } } From 4fc01afbaa3c1d2d64b74082d69020243b7b6882 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 25 Nov 2025 10:40:55 +0200 Subject: [PATCH 28/90] fix: test time extension test --- integration_tests/src/test_suite_map.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index f51874a..e80f5a4 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1182,8 +1182,7 @@ pub fn prepare_function_map() -> HashMap { let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; println!("Waiting for next blocks to check if continoius run fetch account"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + tokio::time::sleep(Duration::from_secs(3 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; let wallet_config = fetch_config().await.unwrap(); let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) From 893011e7bfcddb62e0f04dbda9ee17c397061a91 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 24 Nov 2025 10:07:43 -0300 Subject: [PATCH 29/90] hot fix for integration test --- integration_tests/src/test_suite_map.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 1b0f14c..47bea3a 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1181,8 +1181,7 @@ pub fn prepare_function_map() -> HashMap { let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; println!("Waiting for next blocks to check if continoius run fetch account"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + tokio::time::sleep(Duration::from_secs(8 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) .await From a0e1bdb963564ce8be8ddb3dfd1310d75736a6ab Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 25 Nov 2025 14:45:37 +0200 Subject: [PATCH 30/90] fix: more extension for test --- 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 e80f5a4..2b19992 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1182,7 +1182,7 @@ pub fn prepare_function_map() -> HashMap { let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; println!("Waiting for next blocks to check if continoius run fetch account"); - tokio::time::sleep(Duration::from_secs(3 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + tokio::time::sleep(Duration::from_secs(6 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; let wallet_config = fetch_config().await.unwrap(); let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) From 377937267473a5a96535ad1a9948e38576afe029 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 25 Nov 2025 17:14:19 +0200 Subject: [PATCH 31/90] fix: ci test --- 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 2b19992..d70efe2 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1182,7 +1182,7 @@ pub fn prepare_function_map() -> HashMap { let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; println!("Waiting for next blocks to check if continoius run fetch account"); - tokio::time::sleep(Duration::from_secs(6 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + tokio::time::sleep(Duration::from_secs(8 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; let wallet_config = fetch_config().await.unwrap(); let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) From 819f96da6c0948f775bf6edcf380502b83ee19d0 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 25 Nov 2025 21:01:00 +0200 Subject: [PATCH 32/90] fix: try other tests --- integration_tests/src/test_suite_map.rs | 118 ++++++++++++------------ 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index d70efe2..f5b2ff2 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1136,82 +1136,82 @@ pub fn prepare_function_map() -> HashMap { info!("Success!"); } - #[nssa_integration_test] - pub async fn test_success_private_transfer_to_another_owned_account_cont_run_path() { - info!( - "########## test_success_private_transfer_to_another_owned_account_cont_run_path ##########" - ); - let continious_run_handle = tokio::spawn(wallet::execute_continious_run()); + // #[nssa_integration_test] + // pub async fn test_success_private_transfer_to_another_owned_account_cont_run_path() { + // info!( + // "########## test_success_private_transfer_to_another_owned_account_cont_run_path ##########" + // ); + // let continious_run_handle = tokio::spawn(wallet::execute_continious_run()); - let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); + // let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); + // let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); - let sub_ret = wallet::execute_subcommand(command).await.unwrap(); - let SubcommandReturnValue::RegisterAccount { addr: to_addr } = sub_ret else { - panic!("FAILED TO REGISTER ACCOUNT"); - }; + // let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + // let SubcommandReturnValue::RegisterAccount { addr: to_addr } = sub_ret else { + // panic!("FAILED TO REGISTER ACCOUNT"); + // }; - 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 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 (to_keys, _) = wallet_storage - .storage - .user_data - .user_private_accounts - .get(&to_addr) - .cloned() - .unwrap(); + // let (to_keys, _) = wallet_storage + // .storage + // .user_data + // .user_private_accounts + // .get(&to_addr) + // .cloned() + // .unwrap(); - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: make_private_account_input_from_str(&from.to_string()), - to: None, - to_npk: Some(hex::encode(to_keys.nullifer_public_key.0)), - to_ipk: Some(hex::encode(to_keys.incoming_viewing_public_key.0)), - amount: 100, - }); + // let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + // from: make_private_account_input_from_str(&from.to_string()), + // to: None, + // to_npk: Some(hex::encode(to_keys.nullifer_public_key.0)), + // to_ipk: Some(hex::encode(to_keys.incoming_viewing_public_key.0)), + // amount: 100, + // }); - let sub_ret = wallet::execute_subcommand(command).await.unwrap(); - let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else { - panic!("FAILED TO SEND TX"); - }; + // let sub_ret = wallet::execute_subcommand(command).await.unwrap(); + // let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else { + // panic!("FAILED TO SEND TX"); + // }; - let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; + // let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; - println!("Waiting for next blocks to check if continoius run fetch account"); - tokio::time::sleep(Duration::from_secs(8 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + // println!("Waiting for next blocks to check if continoius run fetch account"); + // tokio::time::sleep(Duration::from_secs(8 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let wallet_config = fetch_config().await.unwrap(); - let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) - .await - .unwrap(); + // let wallet_config = fetch_config().await.unwrap(); + // let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) + // .await + // .unwrap(); - info!("All private accounts data"); - for (addr, (_, acc)) in &wallet_storage.storage.user_data.user_private_accounts { - info!("{addr} :: {acc:#?}"); - } + // info!("All private accounts data"); + // for (addr, (_, acc)) in &wallet_storage.storage.user_data.user_private_accounts { + // info!("{addr} :: {acc:#?}"); + // } - let new_commitment1 = wallet_storage - .get_private_account_commitment(&from) - .unwrap(); - assert_eq!(tx.message.new_commitments[0], new_commitment1); + // let new_commitment1 = wallet_storage + // .get_private_account_commitment(&from) + // .unwrap(); + // assert_eq!(tx.message.new_commitments[0], new_commitment1); - assert_eq!(tx.message.new_commitments.len(), 2); - for commitment in tx.message.new_commitments.into_iter() { - assert!(verify_commitment_is_in_state(commitment, &seq_client).await); - } + // assert_eq!(tx.message.new_commitments.len(), 2); + // for commitment in tx.message.new_commitments.into_iter() { + // assert!(verify_commitment_is_in_state(commitment, &seq_client).await); + // } - let to_res_acc = wallet_storage.get_account_private(&to_addr).unwrap(); + // let to_res_acc = wallet_storage.get_account_private(&to_addr).unwrap(); - assert_eq!(to_res_acc.balance, 100); + // assert_eq!(to_res_acc.balance, 100); - continious_run_handle.abort(); + // continious_run_handle.abort(); - info!("Success!"); - } + // info!("Success!"); + // } #[nssa_integration_test] pub async fn test_success_deshielded_transfer_to_another_account() { From 463942df8034072f475aa0a5b8208e00644fd0e4 Mon Sep 17 00:00:00 2001 From: Pravdyvy <46261001+Pravdyvy@users.noreply.github.com> Date: Wed, 26 Nov 2025 07:07:58 +0200 Subject: [PATCH 33/90] Apply suggestions from code review 1 Co-authored-by: Daniil Polyakov --- integration_tests/src/lib.rs | 2 +- .../key_management/key_tree/chain_index.rs | 28 +++++---------- .../key_management/key_tree/keys_private.rs | 8 ++--- .../key_management/key_tree/keys_public.rs | 8 ++--- .../src/key_management/key_tree/mod.rs | 16 +++------ .../src/key_management/key_tree/traits.rs | 2 +- key_protocol/src/key_management/mod.rs | 4 +-- .../src/key_management/secret_holders.rs | 2 +- key_protocol/src/key_protocol_core/mod.rs | 4 +-- wallet/src/chain_storage/mod.rs | 36 +++++++------------ wallet/src/lib.rs | 6 ++-- 11 files changed, 45 insertions(+), 71 deletions(-) diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 75e39b8..df39ccd 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -56,7 +56,7 @@ fn make_private_account_input_from_str(addr: &str) -> String { pub async fn pre_test( home_dir: PathBuf, ) -> Result<(ServerHandle, JoinHandle>, TempDir)> { - wallet::execute_setup("test_pass".to_string()).await?; + wallet::execute_setup("test_pass".to_owned()).await?; let home_dir_sequencer = home_dir.join("sequencer"); 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..8da2fd6 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -17,25 +17,15 @@ impl FromStr for ChainIndex { type Err = ChainIndexError; fn from_str(s: &str) -> Result { - if !s.starts_with("/") { - return Err(ChainIndexError::NoRootFound); + if !s.starts_with('/') { + return Err(ChainIndexError:NoRootFound); } - - if s == "/" { - return Ok(ChainIndex(vec![])); - } - - let uprooted_substring = s.strip_prefix("/").unwrap(); - - let splitted_chain: Vec<&str> = uprooted_substring.split("/").collect(); - let mut res = vec![]; - - for split_ch in splitted_chain { - let cci = split_ch.parse()?; - res.push(cci); - } - - Ok(Self(res)) + + s + .split("/") + .map(u32::from_str) + .collect() + .map_err(Into::into) } } @@ -68,7 +58,7 @@ impl ChainIndex { ChainIndex(chain) } - pub fn n_th_child(&self, child_id: u32) -> ChainIndex { + pub fn nth_child(&self, child_id: u32) -> ChainIndex { let mut chain = self.0.clone(); chain.push(child_id); diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index 84b44fa..2addd12 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -12,7 +12,7 @@ use crate::key_management::{ pub struct ChildKeysPrivate { pub value: (KeyChain, nssa::Account), pub ccc: [u8; 32], - ///Can be None if root + /// Can be [`None`] if root pub cci: Option, } @@ -49,7 +49,7 @@ impl KeyNode for ChildKeysPrivate { } } - fn n_th_child(&self, cci: u32) -> Self { + fn nth_child(&self, cci: u32) -> Self { let parent_pt = Scalar::from_repr( self.value .0 @@ -109,8 +109,8 @@ impl KeyNode for ChildKeysPrivate { &self.ccc } - fn child_index(&self) -> &Option { - &self.cci + fn child_index(&self) -> Option { + self.cci } fn address(&self) -> nssa::Address { diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs index 7ca6247..2f694af 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -7,7 +7,7 @@ pub struct ChildKeysPublic { pub csk: nssa::PrivateKey, pub cpk: nssa::PublicKey, pub ccc: [u8; 32], - ///Can be None if root + /// Can be [`None`] if root pub cci: Option, } @@ -27,7 +27,7 @@ impl KeyNode for ChildKeysPublic { } } - fn n_th_child(&self, cci: u32) -> Self { + fn nth_child(&self, cci: u32) -> Self { let mut hash_input = vec![]; hash_input.extend_from_slice(self.csk.value()); hash_input.extend_from_slice(&cci.to_le_bytes()); @@ -50,8 +50,8 @@ impl KeyNode for ChildKeysPublic { &self.ccc } - fn child_index(&self) -> &Option { - &self.cci + fn child_index(&self) -> Option { + self.cci } fn address(&self) -> nssa::Address { diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index dcc027b..73f3491 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -31,11 +31,8 @@ impl KeyTree { let root_keys = Node::root(seed_fit); let address = root_keys.address(); - let mut key_map = BTreeMap::new(); - let mut addr_map = HashMap::new(); - - key_map.insert(ChainIndex::root(), root_keys); - addr_map.insert(address, ChainIndex::root()); + let key_map = BTreeMap::from_iter([(ChainIndex::root(), root_keys)]); + let addr_map = HashMap::from_iter([(address, ChainIndex::root())]); Self { key_map, addr_map } } @@ -60,7 +57,8 @@ impl KeyTree { let leftmost_child = parent_id.n_th_child(u32::MIN); if !self.key_map.contains_key(&leftmost_child) { - Some(0) + return Some(0) + } } else { let mut right = u32::MAX - 1; let mut left_border = u32::MIN; @@ -93,11 +91,7 @@ impl KeyTree { } pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option { - if !self.key_map.contains_key(&parent_cci) { - return None; - } - - let father_keys = self.key_map.get(&parent_cci).unwrap(); + let father_keys = self.key_map.get(&parent_cci)?; let next_child_id = self.find_next_last_child_of_id(&parent_cci).unwrap(); let next_cci = parent_cci.n_th_child(next_child_id); diff --git a/key_protocol/src/key_management/key_tree/traits.rs b/key_protocol/src/key_management/key_tree/traits.rs index 662481a..0eb619d 100644 --- a/key_protocol/src/key_management/key_tree/traits.rs +++ b/key_protocol/src/key_management/key_tree/traits.rs @@ -1,7 +1,7 @@ pub trait KeyNode { fn root(seed: [u8; 64]) -> Self; - fn n_th_child(&self, cci: u32) -> Self; + fn nth_child(&self, cci: u32) -> Self; fn chain_code(&self) -> &[u8; 32]; diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index 8a58d4a..bbccd12 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -41,8 +41,8 @@ impl KeyChain { } pub fn new_mnemonic(passphrase: String) -> Self { - //Currently dropping SeedHolder at the end of initialization. - //Now entirely sure if we need it in the future. + // Currently dropping SeedHolder at the end of initialization. + // Not entirely sure if we need it in the future. let seed_holder = SeedHolder::new_mnemonic(passphrase); let secret_spending_key = seed_holder.produce_top_secret_key_holder(); diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index e60a9f5..89808b6 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -45,7 +45,7 @@ impl SeedHolder { } pub fn new_mnemonic(passphrase: String) -> Self { - //Enthropy bytes must be deterministic as well + // Enthropy bytes must be deterministic as well let enthopy_bytes: [u8; 32] = [0; 32]; let mnemonic = Mnemonic::from_entropy(&enthopy_bytes).unwrap(); diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 6ea75db..81ae156 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -14,9 +14,9 @@ pub type PublicKey = AffinePoint; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NSSAUserData { - ///Default public accounts + /// Default public accounts pub default_pub_account_signing_keys: HashMap, - ///Default private accounts + /// Default private accounts pub default_user_private_accounts: HashMap, /// Tree of public keys diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage/mod.rs index 5215d99..cfc0ff0 100644 --- a/wallet/src/chain_storage/mod.rs +++ b/wallet/src/chain_storage/mod.rs @@ -126,17 +126,13 @@ impl WalletChainStore { addr: nssa::Address, account: nssa_core::account::Account, ) { - println!("inserting at address {}, this account {:?}", addr, account); + println!("inserting at address {addr}, this account {account:?}"); - if self - .user_data - .default_user_private_accounts - .contains_key(&addr) - { - self.user_data + let entry = self.user_data .default_user_private_accounts .entry(addr) .and_modify(|data| data.1 = account); + if matches!(entry, Entry::Vacant(_)) { } else { self.user_data .private_key_tree @@ -270,27 +266,21 @@ mod tests { } fn create_sample_persistent_accounts() -> Vec { - let mut accs = vec![]; - let public_data = ChildKeysPublic::root([42; 64]); - - accs.push(PersistentAccountData::Public(PersistentAccountDataPublic { - address: public_data.address(), - chain_index: ChainIndex::root(), - data: public_data, - })); - let private_data = ChildKeysPrivate::root([47; 64]); - - accs.push(PersistentAccountData::Private( - PersistentAccountDataPrivate { + + vec![ + PersistentAccountData::Public(PersistentAccountDataPublic { + address: public_data.address(), + chain_index: ChainIndex::root(), + data: public_data, + }), + PersistentAccountData::Private(PersistentAccountDataPrivate { address: private_data.address(), chain_index: ChainIndex::root(), data: private_data, - }, - )); - - accs + }) + ] } #[test] diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 3b494a9..75d06df 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -249,7 +249,7 @@ pub enum Command { Config(ConfigSubcommand), } -///Represents CLI command for a wallet with setup included +/// Represents CLI command for a wallet with setup included #[derive(Debug, Subcommand, Clone)] #[clap(about)] pub enum OverCommand { @@ -410,12 +410,12 @@ pub async fn parse_block_range( } } - for (_, keys_node) in wallet_core + for keys_node in wallet_core .storage .user_data .private_key_tree .key_map - .iter() + .values() { let acc_addr = keys_node.address(); let key_chain = &keys_node.value.0; From 0d11c3688e58d64ba527a8ab5421064eec481b8f Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 26 Nov 2025 07:18:25 +0200 Subject: [PATCH 34/90] fix: suggestions hotfix --- .../key_management/key_tree/chain_index.rs | 26 ++++++--- .../key_management/key_tree/keys_private.rs | 2 +- .../key_management/key_tree/keys_public.rs | 2 +- .../src/key_management/key_tree/mod.rs | 53 +++++++++---------- .../src/key_management/key_tree/traits.rs | 2 +- wallet/src/chain_storage/mod.rs | 16 +++--- 6 files changed, 56 insertions(+), 45 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 8da2fd6..1ab431a 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -18,14 +18,24 @@ impl FromStr for ChainIndex { fn from_str(s: &str) -> Result { if !s.starts_with('/') { - return Err(ChainIndexError:NoRootFound); + return Err(ChainIndexError::NoRootFound); } - - s - .split("/") - .map(u32::from_str) - .collect() - .map_err(Into::into) + + if s == "/" { + return Ok(ChainIndex(vec![])); + } + + let uprooted_substring = s.strip_prefix("/").unwrap(); + + let splitted_chain: Vec<&str> = uprooted_substring.split("/").collect(); + let mut res = vec![]; + + for split_ch in splitted_chain { + let cci = split_ch.parse()?; + res.push(cci); + } + + Ok(Self(res)) } } @@ -96,7 +106,7 @@ mod tests { #[test] fn test_chain_id_child_correct() { let chain_id = ChainIndex::from_str("/257").unwrap(); - let child = chain_id.n_th_child(3); + let child = chain_id.nth_child(3); assert_eq!(child, ChainIndex::from_str("/257/3").unwrap()); } diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index 2addd12..c29f7d6 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -137,7 +137,7 @@ mod tests { #[test] fn test_keys_deterministic_generation() { let root_keys = ChildKeysPrivate::root([42; 64]); - let child_keys = root_keys.n_th_child(5); + let child_keys = root_keys.nth_child(5); assert_eq!(root_keys.cci, None); assert_eq!(child_keys.cci, Some(5)); diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs index 2f694af..8d31720 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -72,7 +72,7 @@ mod tests { #[test] fn test_keys_deterministic_generation() { let root_keys = ChildKeysPublic::root([42; 64]); - let child_keys = root_keys.n_th_child(5); + let child_keys = root_keys.nth_child(5); assert_eq!(root_keys.cci, None); assert_eq!(child_keys.cci, Some(5)); diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 73f3491..e478ce5 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -54,37 +54,36 @@ impl KeyTree { return None; } - let leftmost_child = parent_id.n_th_child(u32::MIN); + let leftmost_child = parent_id.nth_child(u32::MIN); if !self.key_map.contains_key(&leftmost_child) { - return Some(0) + return Some(0); } - } else { - let mut right = u32::MAX - 1; - let mut left_border = u32::MIN; - let mut right_border = u32::MAX; - loop { - let rightmost_child = parent_id.n_th_child(right); + let mut right = u32::MAX - 1; + let mut left_border = u32::MIN; + let mut right_border = u32::MAX; - let rightmost_ref = self.key_map.get(&rightmost_child); - let rightmost_ref_next = self.key_map.get(&rightmost_child.next_in_line()); + loop { + let rightmost_child = parent_id.nth_child(right); - match (&rightmost_ref, &rightmost_ref_next) { - (Some(_), Some(_)) => { - left_border = right; - right = (right + right_border) / 2; - } - (Some(_), None) => { - break Some(right + 1); - } - (None, None) => { - right_border = right; - right = (left_border + right) / 2; - } - (None, Some(_)) => { - unreachable!(); - } + let rightmost_ref = self.key_map.get(&rightmost_child); + let rightmost_ref_next = self.key_map.get(&rightmost_child.next_in_line()); + + match (&rightmost_ref, &rightmost_ref_next) { + (Some(_), Some(_)) => { + left_border = right; + right = (right + right_border) / 2; + } + (Some(_), None) => { + break Some(right + 1); + } + (None, None) => { + right_border = right; + right = (left_border + right) / 2; + } + (None, Some(_)) => { + unreachable!(); } } } @@ -93,9 +92,9 @@ impl KeyTree { pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option { let father_keys = self.key_map.get(&parent_cci)?; let next_child_id = self.find_next_last_child_of_id(&parent_cci).unwrap(); - let next_cci = parent_cci.n_th_child(next_child_id); + let next_cci = parent_cci.nth_child(next_child_id); - let child_keys = father_keys.n_th_child(next_child_id); + let child_keys = father_keys.nth_child(next_child_id); let address = child_keys.address(); diff --git a/key_protocol/src/key_management/key_tree/traits.rs b/key_protocol/src/key_management/key_tree/traits.rs index 0eb619d..d8b2de9 100644 --- a/key_protocol/src/key_management/key_tree/traits.rs +++ b/key_protocol/src/key_management/key_tree/traits.rs @@ -5,7 +5,7 @@ pub trait KeyNode { fn chain_code(&self) -> &[u8; 32]; - fn child_index(&self) -> &Option; + fn child_index(&self) -> Option; fn address(&self) -> nssa::Address; } diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage/mod.rs index cfc0ff0..c7d1700 100644 --- a/wallet/src/chain_storage/mod.rs +++ b/wallet/src/chain_storage/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, hash_map::Entry}; use anyhow::Result; use key_protocol::{ @@ -128,10 +128,12 @@ impl WalletChainStore { ) { println!("inserting at address {addr}, this account {account:?}"); - let entry = self.user_data - .default_user_private_accounts - .entry(addr) - .and_modify(|data| data.1 = account); + let entry = self + .user_data + .default_user_private_accounts + .entry(addr) + .and_modify(|data| data.1 = account.clone()); + if matches!(entry, Entry::Vacant(_)) { } else { self.user_data @@ -268,7 +270,7 @@ mod tests { fn create_sample_persistent_accounts() -> Vec { let public_data = ChildKeysPublic::root([42; 64]); let private_data = ChildKeysPrivate::root([47; 64]); - + vec![ PersistentAccountData::Public(PersistentAccountDataPublic { address: public_data.address(), @@ -279,7 +281,7 @@ mod tests { address: private_data.address(), chain_index: ChainIndex::root(), data: private_data, - }) + }), ] } From fc531021fb4f5f34b0d5abc3b091775095516499 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 26 Nov 2025 07:32:35 +0200 Subject: [PATCH 35/90] fix: comments fix 1 --- wallet/src/chain_storage/mod.rs | 4 ++-- wallet/src/cli/account.rs | 16 +++++++++------- wallet/src/helperfunctions.rs | 2 +- wallet/src/lib.rs | 11 +++++------ 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage/mod.rs index c7d1700..b5b1c9f 100644 --- a/wallet/src/chain_storage/mod.rs +++ b/wallet/src/chain_storage/mod.rs @@ -36,7 +36,7 @@ impl WalletChainStore { _ => false, }) .cloned() - .unwrap(); + .expect("Malformed persistent account data, must have public root"); let private_root = persistent_accounts .iter() @@ -45,7 +45,7 @@ impl WalletChainStore { _ => false, }) .cloned() - .unwrap(); + .expect("Malformed persistent account data, must have private root"); let mut public_tree = KeyTreePublic::new_from_root(match public_root { PersistentAccountData::Public(data) => data.data, diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index b9a7699..abd7a4f 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -92,11 +92,13 @@ pub enum NewSubcommand { ///Register new public account Public { #[arg(long)] + /// Chain index of a parent node cci: ChainIndex, }, ///Register new private account Private { #[arg(long)] + /// Chain index of a parent node cci: ChainIndex, }, } @@ -274,13 +276,19 @@ impl WalletSubcommand for AccountSubcommand { .await? .last_block; - if !wallet_core + if wallet_core .storage .user_data .private_key_tree .addr_map .is_empty() { + wallet_core.last_synced_block = curr_last_block; + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent data at {path:#?}"); + } else { parse_block_range( last_synced_block + 1, curr_last_block, @@ -288,12 +296,6 @@ impl WalletSubcommand for AccountSubcommand { wallet_core, ) .await?; - } else { - wallet_core.last_synced_block = curr_last_block; - - let path = wallet_core.store_persistent_data().await?; - - println!("Stored persistent data at {path:#?}"); } Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block)) diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 0d08348..f876777 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -91,7 +91,7 @@ pub async fn fetch_config() -> Result { /// Fetch data stored at home /// -/// If file not present, it is considered as empty list of persistent accounts +/// File must be created through setup beforehand. pub async fn fetch_persistent_storage() -> Result { let home = get_home()?; let accs_path = home.join("storage.json"); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 75d06df..8b6b71b 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -249,12 +249,14 @@ pub enum Command { Config(ConfigSubcommand), } -/// Represents CLI command for a wallet with setup included +/// 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, @@ -369,11 +371,8 @@ pub async fn parse_block_range( if let NSSATransaction::PrivacyPreserving(tx) = nssa_tx { let mut affected_accounts = vec![]; - for (acc_addr, (key_chain, _)) in wallet_core - .storage - .user_data - .default_user_private_accounts - .iter() + for (acc_addr, (key_chain, _)) in + &wallet_core.storage.user_data.default_user_private_accounts { let view_tag = EncryptedAccountData::compute_view_tag( key_chain.nullifer_public_key.clone(), From 77570c48e9962d93d903fbbf881c869ef31e12c7 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 26 Nov 2025 14:27:09 +0200 Subject: [PATCH 36/90] fix: suggestions 2 --- .../key_management/key_tree/chain_index.rs | 25 +++++++++++++++++- .../key_management/key_tree/keys_private.rs | 26 ++++++++++++++----- 2 files changed, 43 insertions(+), 8 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 1ab431a..9d641b3 100644 --- a/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/key_protocol/src/key_management/key_tree/chain_index.rs @@ -49,9 +49,15 @@ impl Display for ChainIndex { } } +impl Default for ChainIndex { + fn default() -> Self { + ChainIndex::from_str("/").expect("Root parsing failure") + } +} + impl ChainIndex { pub fn root() -> Self { - ChainIndex::from_str("/").unwrap() + ChainIndex::default() } pub fn chain(&self) -> &[u32] { @@ -95,6 +101,23 @@ mod tests { assert_eq!(chain_id.chain(), &[257]); } + #[test] + fn test_chain_id_deser_failure_no_root() { + let chain_index_error = ChainIndex::from_str("257").err().unwrap(); + + assert!(matches!(chain_index_error, ChainIndexError::NoRootFound)); + } + + #[test] + fn test_chain_id_deser_failure_int_parsing_failure() { + let chain_index_error = ChainIndex::from_str("/hello").err().unwrap(); + + assert!(matches!( + chain_index_error, + ChainIndexError::ParseIntError(_) + )); + } + #[test] fn test_chain_id_next_in_line_correct() { let chain_id = ChainIndex::from_str("/257").unwrap(); diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index c29f7d6..c13a6b3 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -20,8 +20,14 @@ impl KeyNode for ChildKeysPrivate { fn root(seed: [u8; 64]) -> Self { let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_priv"); - let ssk = SecretSpendingKey(*hash_value.first_chunk::<32>().unwrap()); - let ccc = *hash_value.last_chunk::<32>().unwrap(); + let ssk = SecretSpendingKey( + *hash_value + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"), + ); + let ccc = *hash_value + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"); let nsk = ssk.generate_nullifier_secret_key(); let isk = ssk.generate_incoming_viewing_secret_key(); @@ -57,9 +63,9 @@ impl KeyNode for ChildKeysPrivate { .outgoing_viewing_secret_key .into(), ) - .unwrap() + .expect("Key generated as scalar, must be valid representation") + Scalar::from_repr(self.value.0.private_key_holder.nullifier_secret_key.into()) - .unwrap() + .expect("Key generated as scalar, must be valid representation") * Scalar::from_repr( self.value .0 @@ -67,7 +73,7 @@ impl KeyNode for ChildKeysPrivate { .incoming_viewing_secret_key .into(), ) - .unwrap(); + .expect("Key generated as scalar, must be valid representation"); let mut input = vec![]; input.extend_from_slice(b"NSSA_seed_priv"); @@ -76,8 +82,14 @@ impl KeyNode for ChildKeysPrivate { let hash_value = hmac_sha512::HMAC::mac(input, self.ccc); - let ssk = SecretSpendingKey(*hash_value.first_chunk::<32>().unwrap()); - let ccc = *hash_value.last_chunk::<32>().unwrap(); + let ssk = SecretSpendingKey( + *hash_value + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"), + ); + let ccc = *hash_value + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"); let nsk = ssk.generate_nullifier_secret_key(); let isk = ssk.generate_incoming_viewing_secret_key(); From 30f19b245d0c8cf3de93b41aed83aec2dfdb981f Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 26 Nov 2025 14:53:26 +0200 Subject: [PATCH 37/90] fix: suggestions fix 3 --- .../key_management/key_tree/keys_public.rs | 11 +++++-- .../src/key_management/key_tree/mod.rs | 33 ++++++++++--------- .../src/key_management/key_tree/traits.rs | 3 ++ .../src/key_management/secret_holders.rs | 11 ++++--- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs index 8d31720..a7bfc4f 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -34,8 +34,15 @@ impl KeyNode for ChildKeysPublic { let hash_value = hmac_sha512::HMAC::mac(&hash_input, self.ccc); - let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap(); - let ccc = *hash_value.last_chunk::<32>().unwrap(); + let csk = nssa::PrivateKey::try_new( + *hash_value + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"), + ) + .unwrap(); + let ccc = *hash_value + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"); let cpk = nssa::PublicKey::new_from_private_key(&csk); Self { diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index e478ce5..5a32a65 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -16,19 +16,23 @@ pub mod keys_public; pub mod traits; #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct KeyTree { - pub key_map: BTreeMap, +pub struct KeyTree { + pub key_map: BTreeMap, pub addr_map: HashMap, } pub type KeyTreePublic = KeyTree; pub type KeyTreePrivate = KeyTree; -impl KeyTree { +impl KeyTree { pub fn new(seed: &SeedHolder) -> Self { - let seed_fit: [u8; 64] = seed.seed.clone().try_into().unwrap(); + let seed_fit: [u8; 64] = seed + .seed + .clone() + .try_into() + .expect("SeedHolder seed is 64 bytes long"); - let root_keys = Node::root(seed_fit); + let root_keys = N::root(seed_fit); let address = root_keys.address(); let key_map = BTreeMap::from_iter([(ChainIndex::root(), root_keys)]); @@ -37,12 +41,9 @@ impl KeyTree { Self { key_map, addr_map } } - pub fn new_from_root(root: Node) -> Self { - let mut key_map = BTreeMap::new(); - let mut addr_map = HashMap::new(); - - addr_map.insert(root.address(), ChainIndex::root()); - key_map.insert(ChainIndex::root(), root); + pub fn new_from_root(root: N) -> Self { + let addr_map = HashMap::from_iter([(root.address(), ChainIndex::root())]); + let key_map = BTreeMap::from_iter([(ChainIndex::root(), root)]); Self { key_map, addr_map } } @@ -91,7 +92,9 @@ impl KeyTree { pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option { let father_keys = self.key_map.get(&parent_cci)?; - let next_child_id = self.find_next_last_child_of_id(&parent_cci).unwrap(); + 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); @@ -104,19 +107,19 @@ impl KeyTree { Some(address) } - pub fn get_node(&self, addr: nssa::Address) -> Option<&Node> { + pub fn get_node(&self, addr: nssa::Address) -> Option<&N> { self.addr_map .get(&addr) .and_then(|chain_id| self.key_map.get(chain_id)) } - pub fn get_node_mut(&mut self, addr: nssa::Address) -> Option<&mut Node> { + pub fn get_node_mut(&mut self, addr: nssa::Address) -> Option<&mut N> { self.addr_map .get(&addr) .and_then(|chain_id| self.key_map.get_mut(chain_id)) } - pub fn insert(&mut self, addr: nssa::Address, chain_index: ChainIndex, node: Node) { + pub fn insert(&mut self, addr: nssa::Address, chain_index: ChainIndex, node: N) { self.addr_map.insert(addr, chain_index.clone()); self.key_map.insert(chain_index, node); } diff --git a/key_protocol/src/key_management/key_tree/traits.rs b/key_protocol/src/key_management/key_tree/traits.rs index d8b2de9..47b3a12 100644 --- a/key_protocol/src/key_management/key_tree/traits.rs +++ b/key_protocol/src/key_management/key_tree/traits.rs @@ -1,6 +1,9 @@ +/// Trait, that reperesents a Node in hierarchical key tree pub trait KeyNode { + /// Tree root node fn root(seed: [u8; 64]) -> Self; + /// `cci`'s child of node fn nth_child(&self, cci: u32) -> Self; fn chain_code(&self) -> &[u8; 32]; diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index 89808b6..d9dd9fa 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -8,6 +8,8 @@ use rand::{RngCore, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest, digest::FixedOutput}; +const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32]; + #[derive(Debug)] ///Seed holder. Non-clonable to ensure that different holders use different seeds. /// Produces `TopSecretKeyHolder` objects. @@ -36,7 +38,8 @@ impl SeedHolder { let mut enthopy_bytes: [u8; 32] = [0; 32]; OsRng.fill_bytes(&mut enthopy_bytes); - let mnemonic = Mnemonic::from_entropy(&enthopy_bytes).unwrap(); + let mnemonic = Mnemonic::from_entropy(&enthopy_bytes) + .expect("Enthropy must be a multiple of 32 bytes"); let seed_wide = mnemonic.to_seed("mnemonic"); Self { @@ -45,10 +48,8 @@ impl SeedHolder { } pub fn new_mnemonic(passphrase: String) -> Self { - // Enthropy bytes must be deterministic as well - let enthopy_bytes: [u8; 32] = [0; 32]; - - let mnemonic = Mnemonic::from_entropy(&enthopy_bytes).unwrap(); + let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES) + .expect("Enthropy must be a multiple of 32 bytes"); let seed_wide = mnemonic.to_seed(passphrase); Self { From d1d22920286439d39476d4547ebd7df543cebd38 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 26 Nov 2025 20:13:23 -0300 Subject: [PATCH 38/90] fmt --- 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 90d2a9a..cef7791 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2110,7 +2110,7 @@ pub mod tests { let message = public_transaction::Message::try_new( program.id(), vec![to, from], // The chain_caller program permutes the account order in the chain - // call + // call vec![0], instruction, ) @@ -2148,7 +2148,7 @@ pub mod tests { let message = public_transaction::Message::try_new( program.id(), vec![to, from], // The chain_caller program permutes the account order in the chain - // call + // call vec![0], instruction, ) From d7331455fc5cef688087043681b21324033c63c9 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 27 Nov 2025 04:22:49 +0300 Subject: [PATCH 39/90] feat: add list account subcommand --- integration_tests/src/test_suite_map.rs | 4 ++-- wallet/Cargo.toml | 1 + wallet/src/cli/account.rs | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 2fa8e1d..648e8a5 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1169,8 +1169,8 @@ pub fn prepare_function_map() -> HashMap { // #[nssa_integration_test] // pub async fn test_success_private_transfer_to_another_owned_account_cont_run_path() { // info!( - // "########## test_success_private_transfer_to_another_owned_account_cont_run_path ##########" - // ); + // "########## test_success_private_transfer_to_another_owned_account_cont_run_path + // ##########" ); // let continious_run_handle = tokio::spawn(wallet::execute_continious_run()); // let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap(); diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index b04d67e..74eb5bc 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -19,6 +19,7 @@ borsh.workspace = true base58.workspace = true hex = "0.4.3" rand.workspace = true +itertools = "0.14.0" [dependencies.key_protocol] path = "../key_protocol" diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 79371c8..d1e361a 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -1,6 +1,7 @@ use anyhow::Result; use base58::ToBase58; use clap::Subcommand; +use itertools::Itertools as _; use nssa::{Account, AccountId, program::Program}; use serde::Serialize; @@ -83,6 +84,9 @@ pub enum AccountSubcommand { New(NewSubcommand), /// Sync private accounts SyncPrivate {}, + /// List all accounts owned by the wallet + #[command(visible_alias = "ls")] + List {}, } /// Represents generic register CLI subcommand @@ -294,6 +298,23 @@ impl WalletSubcommand for AccountSubcommand { Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block)) } + AccountSubcommand::List {} => { + let user_data = &wallet_core.storage.user_data; + let accounts = user_data + .pub_account_signing_keys + .keys() + .map(|id| format!("Public/{id}")) + .chain( + user_data + .user_private_accounts + .keys() + .map(|id| format!("Private/{id}")), + ) + .format(",\n"); + + println!("{accounts}"); + Ok(SubcommandReturnValue::Empty) + } } } } From bce0ac79cb415a7702d542e6c71d4d1bdf4e5fcc Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Thu, 27 Nov 2025 05:55:25 +0200 Subject: [PATCH 40/90] fix: fmt fix --- integration_tests/src/test_suite_map.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 2fa8e1d..648e8a5 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1169,8 +1169,8 @@ pub fn prepare_function_map() -> HashMap { // #[nssa_integration_test] // pub async fn test_success_private_transfer_to_another_owned_account_cont_run_path() { // info!( - // "########## test_success_private_transfer_to_another_owned_account_cont_run_path ##########" - // ); + // "########## test_success_private_transfer_to_another_owned_account_cont_run_path + // ##########" ); // let continious_run_handle = tokio::spawn(wallet::execute_continious_run()); // let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap(); From 00386eba240eb9a71ce6386ea3a118cc735bee19 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Thu, 27 Nov 2025 06:03:43 +0200 Subject: [PATCH 41/90] fix: fmt --- integration_tests/src/test_suite_map.rs | 4 ++-- nssa/core/src/account.rs | 8 ++++---- nssa/core/src/encryption/shared_key_derivation.rs | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 2fa8e1d..648e8a5 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1169,8 +1169,8 @@ pub fn prepare_function_map() -> HashMap { // #[nssa_integration_test] // pub async fn test_success_private_transfer_to_another_owned_account_cont_run_path() { // info!( - // "########## test_success_private_transfer_to_another_owned_account_cont_run_path ##########" - // ); + // "########## test_success_private_transfer_to_another_owned_account_cont_run_path + // ##########" ); // let continious_run_handle = tokio::spawn(wallet::execute_continious_run()); // let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap(); diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index e767e1e..f32d05d 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -1,12 +1,12 @@ -use crate::program::ProgramId; -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; - #[cfg(feature = "host")] use std::{fmt::Display, str::FromStr}; #[cfg(feature = "host")] use base58::{FromBase58, ToBase58}; +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use crate::program::ProgramId; pub type Nonce = u128; pub type Data = Vec; diff --git a/nssa/core/src/encryption/shared_key_derivation.rs b/nssa/core/src/encryption/shared_key_derivation.rs index 7349d70..b1a572e 100644 --- a/nssa/core/src/encryption/shared_key_derivation.rs +++ b/nssa/core/src/encryption/shared_key_derivation.rs @@ -1,5 +1,4 @@ use borsh::{BorshDeserialize, BorshSerialize}; - use k256::{ AffinePoint, EncodedPoint, FieldBytes, ProjectivePoint, elliptic_curve::{ From 577fad6d5f5ae87cf83e5b0b66ee653d8b29f52e Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 09:54:14 -0300 Subject: [PATCH 42/90] refactor call stack execution loop --- nssa/src/public_transaction/transaction.rs | 45 +++++++++++----------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 61c625e..a399ad3 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, - program::{DEFAULT_PROGRAM_ID, validate_execution}, + program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; @@ -88,7 +88,7 @@ impl PublicTransaction { } // Build pre_states for execution - let mut input_pre_states: Vec<_> = message + let input_pre_states: Vec<_> = message .account_ids .iter() .map(|account_id| { @@ -102,17 +102,27 @@ impl PublicTransaction { let mut state_diff: HashMap = HashMap::new(); - let mut program_id = message.program_id; - let mut instruction_data = message.instruction_data.clone(); - let mut chained_calls = Vec::new(); + let initial_call = ChainedCall { + program_id: message.program_id, + instruction_data: message.instruction_data.clone(), + pre_states: input_pre_states, + }; + + let mut chained_calls = Vec::from_iter([initial_call]); + let mut chain_calls_counter = 0; + + while let Some(chained_call) = chained_calls.pop() { + if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS { + return Err(NssaError::MaxChainedCallsDepthExceeded); + } - for _i in 0..MAX_NUMBER_CHAINED_CALLS { // Check the `program_id` corresponds to a deployed program - let Some(program) = state.programs().get(&program_id) else { + let Some(program) = state.programs().get(&chained_call.program_id) else { return Err(NssaError::InvalidInput("Unknown program".into())); }; - let mut program_output = program.execute(&input_pre_states, &instruction_data)?; + let mut program_output = + program.execute(&chained_call.pre_states, &chained_call.instruction_data)?; for pre in program_output.pre_states.iter() { let account_id = pre.account_id; @@ -137,7 +147,7 @@ impl PublicTransaction { if !validate_execution( &program_output.pre_states, &program_output.post_states, - program_id, + chained_call.program_id, ) { return Err(NssaError::InvalidProgramBehavior); } @@ -145,7 +155,7 @@ impl PublicTransaction { // The invoked program claims the accounts with default program id. for post in program_output.post_states.iter_mut() { if post.program_owner == DEFAULT_PROGRAM_ID { - post.program_owner = program_id; + post.program_owner = chained_call.program_id; } } @@ -159,21 +169,10 @@ impl PublicTransaction { } chained_calls.extend_from_slice(&program_output.chained_calls); - - if let Some(next_chained_call) = chained_calls.pop() { - program_id = next_chained_call.program_id; - instruction_data = next_chained_call.instruction_data; - input_pre_states = next_chained_call.pre_states; - } else { - break; - }; + chain_calls_counter += 1; } - if chained_calls.is_empty() { - Ok(state_diff) - } else { - Err(NssaError::MaxChainedCallsDepthExceeded) - } + Ok(state_diff) } } From 103332f6cd15131679493f6e0176b05980f9086d Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 09:56:52 -0300 Subject: [PATCH 43/90] nit --- nssa/core/src/program.rs | 4 ++-- nssa/src/error.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index f3aaf36..054f993 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -50,12 +50,12 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec pub fn write_nssa_outputs_with_chained_call( pre_states: Vec, post_states: Vec, - chained_call: Vec, + chained_calls: Vec, ) { let output = ProgramOutput { pre_states, post_states, - chained_calls: chained_call, + chained_calls, }; env::commit(&output); } diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 2299731..45d5310 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -55,6 +55,6 @@ pub enum NssaError { #[error("Program already exists")] ProgramAlreadyExists, - #[error("Chain of calls too long")] + #[error("Chain of calls is too long")] MaxChainedCallsDepthExceeded, } From aba1d844f806c6d174ad2b8899d4639a8c8a4840 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 10:42:58 -0300 Subject: [PATCH 44/90] consume call stack from the other end --- nssa/src/public_transaction/transaction.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index a399ad3..af07895 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap, HashSet, VecDeque}; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, @@ -108,10 +108,10 @@ impl PublicTransaction { pre_states: input_pre_states, }; - let mut chained_calls = Vec::from_iter([initial_call]); + let mut chained_calls = VecDeque::from_iter([initial_call]); let mut chain_calls_counter = 0; - while let Some(chained_call) = chained_calls.pop() { + while let Some(chained_call) = chained_calls.pop_front() { if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS { return Err(NssaError::MaxChainedCallsDepthExceeded); } @@ -168,7 +168,7 @@ impl PublicTransaction { state_diff.insert(pre.account_id, post.clone()); } - chained_calls.extend_from_slice(&program_output.chained_calls); + chained_calls.extend(program_output.chained_calls); chain_calls_counter += 1; } From f46d7ee4268ab18562a1353bbbba9031c0e67a4b Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 10:46:35 -0300 Subject: [PATCH 45/90] nit --- nssa/src/public_transaction/transaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index af07895..bce1703 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -124,7 +124,7 @@ impl PublicTransaction { let mut program_output = program.execute(&chained_call.pre_states, &chained_call.instruction_data)?; - for pre in program_output.pre_states.iter() { + for pre in &program_output.pre_states { let account_id = pre.account_id; // Check that the program output pre_states coinicide with the values in the public // state or with any modifications to those values during the chain of calls. From 409ec199590117bdd0ab9ddd34ccdd9b26c2e954 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 10:53:25 -0300 Subject: [PATCH 46/90] fix concatenation of call stack --- nssa/src/public_transaction/transaction.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index bce1703..f10a266 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -168,7 +168,10 @@ impl PublicTransaction { state_diff.insert(pre.account_id, post.clone()); } - chained_calls.extend(program_output.chained_calls); + for new_call in program_output.chained_calls.into_iter().rev() { + chained_calls.push_front(new_call); + } + chain_calls_counter += 1; } From d82f06593da312dba61757f17357c8855b9f3b87 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 12:08:27 -0300 Subject: [PATCH 47/90] add pda_seeds field --- nssa/core/src/program.rs | 5 +++++ nssa/src/public_transaction/transaction.rs | 1 + nssa/test_program_methods/guest/src/bin/chain_caller.rs | 2 ++ 3 files changed, 8 insertions(+) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 054f993..927d5fc 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -12,12 +12,17 @@ pub struct ProgramInput { pub instruction: T, } +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +pub struct PdaSeed([u8; 32]); + #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ChainedCall { pub program_id: ProgramId, pub instruction_data: InstructionData, pub pre_states: Vec, + pub pda_seeds: Vec } #[derive(Serialize, Deserialize, Clone)] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 28f33fb..081fe2f 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -107,6 +107,7 @@ impl PublicTransaction { program_id: message.program_id, instruction_data: message.instruction_data.clone(), pre_states: input_pre_states, + pda_seeds: vec![], }; let mut chained_calls = VecDeque::from_iter([initial_call]); diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index 028f8a0..23b0244 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -25,6 +25,7 @@ fn main() { program_id, instruction_data: instruction_data.clone(), pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here + pda_seeds: vec![] }; num_chain_calls as usize - 1 ]; @@ -33,6 +34,7 @@ fn main() { program_id, instruction_data, pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here + pda_seeds: vec![], }); write_nssa_outputs_with_chained_call( From 3fbf1e1fec33e57de6c0018d695ffa4f695e5033 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 13:10:38 -0300 Subject: [PATCH 48/90] add pda mechanism --- nssa/core/src/program.rs | 26 +++++++++++++++-- nssa/src/public_transaction/transaction.rs | 33 ++++++++++++++++++---- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 927d5fc..ad9bbab 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,7 +1,7 @@ use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; -use crate::account::{Account, AccountWithMetadata}; +use crate::account::{Account, AccountId, AccountWithMetadata}; pub type ProgramId = [u32; 8]; pub type InstructionData = Vec; @@ -16,13 +16,35 @@ pub struct ProgramInput { #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct PdaSeed([u8; 32]); +#[cfg(feature = "host")] +impl From<(&ProgramId, &PdaSeed)> for AccountId { + fn from(value: (&ProgramId, &PdaSeed)) -> Self { + use risc0_zkvm::sha::{Impl, Sha256}; + const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] = + b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00"; + + let mut bytes = [0; 96]; + bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX); + let program_id_bytes: &[u8] = + bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[u8]"); + bytes[32..64].copy_from_slice(program_id_bytes); + bytes[64..].copy_from_slice(&value.1.0); + AccountId::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) + } +} + #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ChainedCall { pub program_id: ProgramId, pub instruction_data: InstructionData, pub pre_states: Vec, - pub pda_seeds: Vec + pub pda_seeds: Vec, } #[derive(Serialize, Deserialize, Clone)] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 081fe2f..cafa27b 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, - program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, + program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; @@ -110,10 +110,10 @@ impl PublicTransaction { pda_seeds: vec![], }; - let mut chained_calls = VecDeque::from_iter([initial_call]); + let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); let mut chain_calls_counter = 0; - while let Some(chained_call) = chained_calls.pop_front() { + while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() { if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS { return Err(NssaError::MaxChainedCallsDepthExceeded); } @@ -126,6 +126,9 @@ impl PublicTransaction { let mut program_output = program.execute(&chained_call.pre_states, &chained_call.instruction_data)?; + let authorized_pdas = + self.compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds); + for pre in &program_output.pre_states { let account_id = pre.account_id; // Check that the program output pre_states coinicide with the values in the public @@ -138,8 +141,11 @@ impl PublicTransaction { return Err(NssaError::InvalidProgramBehavior); } - // Check that authorization flags are consistent with the provided ones - if pre.is_authorized && !signer_account_ids.contains(&account_id) { + // Check that authorization flags are consistent with the provided ones or + // authorized by program through the PDA mechanism + let is_authorized = signer_account_ids.contains(&account_id) + || authorized_pdas.contains(&account_id); + if pre.is_authorized && !is_authorized { return Err(NssaError::InvalidProgramBehavior); } } @@ -171,7 +177,7 @@ impl PublicTransaction { } for new_call in program_output.chained_calls.into_iter().rev() { - chained_calls.push_front(new_call); + chained_calls.push_front((new_call, Some(chained_call.program_id))); } chain_calls_counter += 1; @@ -179,6 +185,21 @@ impl PublicTransaction { Ok(state_diff) } + + fn compute_authorized_pdas( + &self, + caller_program_id: &Option, + pda_seeds: &[PdaSeed], + ) -> HashSet { + if let Some(caller_program_id) = caller_program_id { + pda_seeds + .iter() + .map(|pda_seed| AccountId::from((caller_program_id, pda_seed))) + .collect() + } else { + HashSet::new() + } + } } #[cfg(test)] From e61a971790cf5ba9cde4e7de9a34b401f2460aff Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 13:49:56 -0300 Subject: [PATCH 49/90] add test and refactor chain_caller program --- nssa/core/src/program.rs | 6 ++ nssa/src/state.rs | 60 +++++++++++++++++-- .../guest/src/bin/chain_caller.rs | 50 +++++++++------- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index ad9bbab..dfd52e4 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -16,6 +16,12 @@ pub struct ProgramInput { #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct PdaSeed([u8; 32]); +impl PdaSeed { + pub fn new(value: [u8; 32]) -> Self { + Self(value) + } +} + #[cfg(feature = "host")] impl From<(&ProgramId, &PdaSeed)> for AccountId { fn from(value: (&ProgramId, &PdaSeed)) -> Self { diff --git a/nssa/src/state.rs b/nssa/src/state.rs index cef7791..79541a3 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -250,7 +250,7 @@ pub mod tests { Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce}, encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar}, - program::ProgramId, + program::{PdaSeed, ProgramId}, }; use crate::{ @@ -2092,14 +2092,18 @@ pub mod tests { let key = PrivateKey::try_new([1; 32]).unwrap(); let from = AccountId::from(&PublicKey::new_from_private_key(&key)); let to = AccountId::new([2; 32]); - let initial_balance = 100; + let initial_balance = 1000; let initial_data = [(from, initial_balance), (to, 0)]; let mut state = V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); let from_key = key; - let amount: u128 = 0; - let instruction: (u128, ProgramId, u32) = - (amount, Program::authenticated_transfer_program().id(), 2); + let amount: u128 = 37; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + Program::authenticated_transfer_program().id(), + 2, + None, + ); let expected_to_post = Account { program_owner: Program::authenticated_transfer_program().id(), @@ -2139,10 +2143,11 @@ pub mod tests { V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); let from_key = key; let amount: u128 = 0; - let instruction: (u128, ProgramId, u32) = ( + let instruction: (u128, ProgramId, u32, Option) = ( amount, Program::authenticated_transfer_program().id(), MAX_NUMBER_CHAINED_CALLS as u32 + 1, + None, ); let message = public_transaction::Message::try_new( @@ -2162,4 +2167,47 @@ pub mod tests { Err(NssaError::MaxChainedCallsDepthExceeded) )); } + #[test] + fn test_execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() { + let chain_caller = Program::chain_caller(); + let pda_seed = PdaSeed::new([37; 32]); + let from = AccountId::from((&chain_caller.id(), &pda_seed)); + let to = AccountId::new([2; 32]); + let initial_balance = 1000; + let initial_data = [(from, initial_balance), (to, 0)]; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let amount: u128 = 58; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + Program::authenticated_transfer_program().id(), + 1, + Some(pda_seed), + ); + + let expected_to_post = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: amount, // The `chain_caller` chains the program twice + ..Account::default() + }; + + let message = public_transaction::Message::try_new( + chain_caller.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx).unwrap(); + + let from_post = state.get_account_by_id(&from); + let to_post = state.get_account_by_id(&to); + // The `chain_caller` program calls the program twice + assert_eq!(from_post.balance, initial_balance - amount); + assert_eq!(to_post, expected_to_post); + } } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index 23b0244..1885da2 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -1,45 +1,51 @@ use nssa_core::program::{ - ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call, + ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, }; use risc0_zkvm::serde::to_vec; -type Instruction = (u128, ProgramId, u32); +type Instruction = (u128, ProgramId, u32, Option); /// A program that calls another program `num_chain_calls` times. /// It permutes the order of the input accounts on the subsequent call +/// The `ProgramId` in the instruction must be the program_id of the authenticated transfers program fn main() { let ProgramInput { pre_states, - instruction: (balance, program_id, num_chain_calls), + instruction: (balance, auth_transfer_id, num_chain_calls, pda_seed), } = read_nssa_inputs::(); - let [sender_pre, receiver_pre] = match pre_states.try_into() { + let [recipient_pre, sender_pre] = match pre_states.try_into() { Ok(array) => array, Err(_) => return, }; let instruction_data = to_vec(&balance).unwrap(); - let mut chained_call = vec![ - ChainedCall { - program_id, - instruction_data: instruction_data.clone(), - pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here - pda_seeds: vec![] - }; - num_chain_calls as usize - 1 - ]; + let mut running_recipient_pre = recipient_pre.clone(); + let mut running_sender_pre = sender_pre.clone(); - chained_call.push(ChainedCall { - program_id, - instruction_data, - pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here - pda_seeds: vec![], - }); + if pda_seed.is_some() { + running_sender_pre.is_authorized = true; + } + + let mut chained_calls = Vec::new(); + for _i in 0..num_chain_calls { + let new_chained_call = ChainedCall { + program_id: auth_transfer_id, + instruction_data: instruction_data.clone(), + pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], // <- Account order permutation here + pda_seeds: pda_seed.iter().cloned().collect(), + }; + chained_calls.push(new_chained_call); + + running_sender_pre.account.balance -= balance; + running_recipient_pre.account.balance += balance; + } write_nssa_outputs_with_chained_call( - vec![sender_pre.clone(), receiver_pre.clone()], - vec![sender_pre.account, receiver_pre.account], - chained_call, + vec![recipient_pre.clone(), sender_pre.clone()], + vec![recipient_pre.account, sender_pre.account], + chained_calls, ); } From 64e2fb73a89b482b9e61231300cf28b2cda88de0 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 28 Nov 2025 08:48:11 +0200 Subject: [PATCH 50/90] fix: merge update --- integration_tests/src/test_suite_map.rs | 2 +- key_protocol/src/key_management/key_tree/chain_index.rs | 7 +++++-- wallet/src/chain_storage/mod.rs | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 7706b1b..1cf71ad 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1583,7 +1583,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_pinata_private_receiver_new_account() { - info!("########## test_pinata_private_receiver ##########"); + info!("########## test_pinata_private_receiver_new_account ##########"); let pinata_account_id = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; 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 9ba9986..e46fc0f 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)] { + for cci in &self.0[..(self.0.len().saturating_sub(1))] { write!(f, "{cci}/")?; } - write!(f, "{}", self.0.last().unwrap()) + if let Some(last) = self.0.last() { + write!(f, "{}", last)?; + } + Ok(()) } } diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage/mod.rs index 00378f1..14e931a 100644 --- a/wallet/src/chain_storage/mod.rs +++ b/wallet/src/chain_storage/mod.rs @@ -136,7 +136,6 @@ impl WalletChainStore { .and_modify(|data| data.1 = account.clone()); if matches!(entry, Entry::Vacant(_)) { - } else { self.user_data .private_key_tree .account_id_map From c7ce31d00c4785ed68d6732adc36ba4a13c680d0 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Fri, 28 Nov 2025 09:49:05 +0200 Subject: [PATCH 51/90] fix: fmt --- wallet/src/cli/account.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index ee71f82..6e831d1 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -1,8 +1,8 @@ use anyhow::Result; use base58::ToBase58; use clap::Subcommand; -use key_protocol::key_management::key_tree::chain_index::ChainIndex; use itertools::Itertools as _; +use key_protocol::key_management::key_tree::chain_index::ChainIndex; use nssa::{Account, AccountId, program::Program}; use serde::Serialize; From 1989fd25a178eca653992ef3c47e1541eb490378 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 28 Nov 2025 11:10:00 -0300 Subject: [PATCH 52/90] add pinata token example and test --- nssa/core/Cargo.toml | 2 +- .../guest/src/bin/pinata_token.rs | 103 ++++++++++++++++++ nssa/program_methods/guest/src/bin/token.rs | 49 ++++++++- nssa/src/program.rs | 5 + nssa/src/state.rs | 91 ++++++++++++++++ 5 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 nssa/program_methods/guest/src/bin/pinata_token.rs diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index 67f40b2..0e16a3f 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -12,7 +12,7 @@ chacha20 = { version = "0.9", default-features = false } k256 = { version = "0.13.3", optional = true } base58 = { version = "0.2.0", optional = true } anyhow = { version = "1.0.98", optional = true } -borsh.workspace = true +borsh = "1.5.7" [features] default = [] diff --git a/nssa/program_methods/guest/src/bin/pinata_token.rs b/nssa/program_methods/guest/src/bin/pinata_token.rs new file mode 100644 index 0000000..ab04237 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/pinata_token.rs @@ -0,0 +1,103 @@ +use nssa_core::program::{ + ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call, +}; +use risc0_zkvm::serde::to_vec; +use risc0_zkvm::sha::{Impl, Sha256}; + +const PRIZE: u128 = 150; + +type Instruction = u128; + +struct Challenge { + difficulty: u8, + seed: [u8; 32], +} + +impl Challenge { + fn new(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 33); + let difficulty = bytes[0]; + assert!(difficulty <= 32); + + let mut seed = [0; 32]; + seed.copy_from_slice(&bytes[1..]); + Self { difficulty, seed } + } + + // Checks if the leftmost `self.difficulty` number of bytes of SHA256(self.data || solution) are + // zero. + fn validate_solution(&self, solution: Instruction) -> bool { + let mut bytes = [0; 32 + 16]; + bytes[..32].copy_from_slice(&self.seed); + bytes[32..].copy_from_slice(&solution.to_le_bytes()); + let digest: [u8; 32] = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap(); + let difficulty = self.difficulty as usize; + digest[..difficulty].iter().all(|&b| b == 0) + } + + fn next_data(self) -> [u8; 33] { + let mut result = [0; 33]; + result[0] = self.difficulty; + result[1..].copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes()); + result + } +} + +/// A pinata program +fn main() { + // Read input accounts. + // It is expected to receive three accounts: [pinata_definition, pinata_token_holding, winner_token_holding] + let ProgramInput { + pre_states, + instruction: solution, + } = read_nssa_inputs::(); + + let [ + pinata_definition, + pinata_token_holding, + winner_token_holding, + ] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let data = Challenge::new(&pinata_definition.account.data); + + if !data.validate_solution(solution) { + return; + } + + 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(); + + let mut instruction_data: [u8; 23] = [0; 23]; + instruction_data[0] = 1; + instruction_data[1..17].copy_from_slice(&PRIZE.to_le_bytes()); + + // Flip authorization to true for chained call + let mut pinata_token_holding_for_chain_call = pinata_token_holding.clone(); + pinata_token_holding_for_chain_call.is_authorized = true; + + 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()], + pda_seeds: vec![PdaSeed::new([0; 32])], + }]; + + write_nssa_outputs_with_chained_call( + vec![ + pinata_definition, + pinata_token_holding, + winner_token_holding, + ], + vec![ + pinata_definition_post, + pinata_token_holding_post, + winner_token_holding_post, + ], + chained_calls, + ); +} diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index e5680be..59aae6a 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -45,6 +45,21 @@ impl TokenDefinition { bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); bytes.into() } + + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_DEFINITION_DATA_SIZE || data[0] != TOKEN_DEFINITION_TYPE { + None + } else { + let account_type = data[0]; + let name = data[1..7].try_into().unwrap(); + let total_supply = u128::from_le_bytes(data[7..].try_into().unwrap()); + Some(Self { + account_type, + name, + total_supply, + }) + } + } } impl TokenHolding { @@ -196,15 +211,47 @@ fn main() { let post_states = transfer(&pre_states, balance_to_move); write_nssa_outputs(pre_states, post_states); } + 2 => { + // Initialize account + assert_eq!(instruction[1..], [0; 22]); + let post_states = initialize(&pre_states); + write_nssa_outputs(pre_states, post_states); + } _ => panic!("Invalid instruction"), }; } +fn initialize(pre_states: &[AccountWithMetadata]) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of accounts"); + } + + let definition = &pre_states[0]; + let account_to_initialize = &pre_states[1]; + + if account_to_initialize.account != Account::default() { + panic!("Only uninitialized accounts can be initialized"); + } + + // TODO: We should check that this is an account owned by the token program. + // This check can't be done here since the ID of the program is known only after compiling it + // Check definition account is valid + let _definition_values = + TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); + let holding_for_definition = TokenHolding::new(&definition.account_id); + + let definition_post = definition.account.clone(); + let mut account_to_initialize_post = account_to_initialize.account.clone(); + account_to_initialize_post.data = holding_for_definition.into_data(); + + vec![definition_post, account_to_initialize_post] +} + #[cfg(test)] mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; - use crate::{new_definition, transfer, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE}; + use crate::{TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE, new_definition, transfer}; #[should_panic(expected = "Invalid number of input accounts")] #[test] diff --git a/nssa/src/program.rs b/nssa/src/program.rs index d3f28b5..4d2232d 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -104,6 +104,11 @@ impl Program { // `program_methods` Self::new(PINATA_ELF.to_vec()).unwrap() } + + pub fn pinata_token() -> Self { + use crate::program_methods::PINATA_TOKEN_ELF; + Self::new(PINATA_TOKEN_ELF.to_vec()).unwrap() + } } #[cfg(test)] diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 79541a3..78b8b7b 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -239,6 +239,20 @@ impl V02State { }, ); } + + pub fn add_pinata_token_program(&mut self, account_id: AccountId) { + self.insert_program(Program::pinata_token()); + + self.public_state.insert( + account_id, + Account { + program_owner: Program::pinata_token().id(), + // Difficulty: 3 + data: vec![3; 33], + ..Account::default() + }, + ); + } } #[cfg(test)] @@ -2210,4 +2224,81 @@ pub mod tests { assert_eq!(from_post.balance, initial_balance - amount); assert_eq!(to_post, expected_to_post); } + + #[test] + fn test_pda_mechanism_with_pinata_token_program() { + let pinata_token = Program::pinata_token(); + let token = Program::token(); + + let pinata_definition_id = AccountId::new([1; 32]); + let pinata_token_definition_id = AccountId::new([2; 32]); + // Total supply of pinata token will be in an account under a PDA. + let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32]))); + let winner_token_holding_id = AccountId::new([3; 32]); + + let mut expected_winner_account_data = [0; 49]; + expected_winner_account_data[0] = 1; + expected_winner_account_data[1..33].copy_from_slice(pinata_token_definition_id.value()); + 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(), + ..Account::default() + }; + + let mut state = V02State::new_with_genesis_accounts(&[], &[]); + state.add_pinata_token_program(pinata_definition_id); + + // Execution of the token program to create new token for the pinata token + // definition and supply accounts + let total_supply: u128 = 10_000_000; + // instruction: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] + let mut instruction: [u8; 23] = [0; 23]; + instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); + instruction[17..].copy_from_slice(b"PINATA"); + let message = public_transaction::Message::try_new( + token.id(), + vec![pinata_token_definition_id, pinata_token_holding_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + // Execution of the token program transfer just to initialize the winner token account + let mut instruction: [u8; 23] = [0; 23]; + instruction[0] = 2; + let message = public_transaction::Message::try_new( + token.id(), + vec![pinata_token_definition_id, winner_token_holding_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + // Submit a solution to the pinata program to claim the prize + let solution: u128 = 989106; + let message = public_transaction::Message::try_new( + pinata_token.id(), + vec![ + pinata_definition_id, + pinata_token_holding_id, + winner_token_holding_id, + ], + vec![], + solution, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let winner_token_holding_post = state.get_account_by_id(&winner_token_holding_id); + assert_eq!(winner_token_holding_post, expected_winner_token_holding_post); + } } From dd5db5b32c77dc62a3dd872e0301d7e96d222eb8 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 28 Nov 2025 11:37:49 -0300 Subject: [PATCH 53/90] add init function to the token program --- nssa/program_methods/guest/src/bin/token.rs | 135 ++++++++++++++++++-- 1 file changed, 125 insertions(+), 10 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index e5680be..e7f7e43 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -18,6 +18,11 @@ use nssa_core::{ // * 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: +// * Two accounts: [definition_account, account_to_initialize]. +// * 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; @@ -45,6 +50,25 @@ impl TokenDefinition { bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); bytes.into() } + + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_DEFINITION_DATA_SIZE || data[0] != TOKEN_DEFINITION_TYPE { + None + } else { + let account_type = data[0]; + let name = data[1..7].try_into().unwrap(); + let total_supply = u128::from_le_bytes( + data[7..] + .try_into() + .expect("Total supply must be 16 bytes little-endian"), + ); + Some(Self { + account_type, + name, + total_supply, + }) + } + } } impl TokenHolding { @@ -61,8 +85,16 @@ impl TokenHolding { None } else { let account_type = data[0]; - let definition_id = AccountId::new(data[1..33].try_into().unwrap()); - let balance = u128::from_le_bytes(data[33..].try_into().unwrap()); + let definition_id = AccountId::new( + data[1..33] + .try_into() + .expect("Defintion ID must be 32 bytes long"), + ); + let balance = u128::from_le_bytes( + data[33..] + .try_into() + .expect("balance must be 16 bytes little-endian"), + ); Some(Self { definition_id, balance, @@ -167,6 +199,33 @@ fn new_definition( vec![definition_target_account_post, holding_target_account_post] } +fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of accounts"); + } + + let definition = &pre_states[0]; + let account_to_initialize = &pre_states[1]; + + if account_to_initialize.account != Account::default() { + panic!("Only uninitialized accounts can be initialized"); + } + + // TODO: We should check that this is an account owned by the token program. + // This check can't be done here since the ID of the program is known only after compiling it + // + // Check definition account is valid + let _definition_values = + TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); + let holding_values = TokenHolding::new(&definition.account_id); + + let definition_post = definition.account.clone(); + let mut account_to_initialize_post = account_to_initialize.account.clone(); + account_to_initialize_post.data = holding_values.into_data(); + + vec![definition_post, account_to_initialize_post] +} + type Instruction = [u8; 23]; fn main() { @@ -175,36 +234,59 @@ fn main() { instruction, } = read_nssa_inputs::(); - match instruction[0] { + let (pre_states, post_states) = match instruction[0] { 0 => { // Parse instruction - let total_supply = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); - let name: [u8; 6] = instruction[17..].try_into().unwrap(); + let total_supply = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("Total supply must be 16 bytes little-endian"), + ); + let name: [u8; 6] = instruction[17..] + .try_into() + .expect("Name must be 6 bytes long"); assert_ne!(name, [0; 6]); // Execute let post_states = new_definition(&pre_states, name, total_supply); - write_nssa_outputs(pre_states, post_states); + (pre_states, post_states) } 1 => { // Parse instruction - let balance_to_move = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); - let name: [u8; 6] = instruction[17..].try_into().unwrap(); + let balance_to_move = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("Balance to move must be 16 bytes little-endian"), + ); + let name: [u8; 6] = instruction[17..] + .try_into() + .expect("Name must be 6 bytes long"); assert_eq!(name, [0; 6]); // Execute let post_states = transfer(&pre_states, balance_to_move); - write_nssa_outputs(pre_states, post_states); + (pre_states, post_states) + } + 2 => { + // Initialize account + assert_eq!(instruction[1..], [0; 22]); + let post_states = initialize_account(&pre_states); + (pre_states, post_states) } _ => panic!("Invalid instruction"), }; + + write_nssa_outputs(pre_states, post_states); } #[cfg(test)] mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; - use crate::{new_definition, transfer, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE}; + use crate::{ + TOKEN_DEFINITION_DATA_SIZE, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE, + initialize_account, new_definition, transfer, + }; #[should_panic(expected = "Invalid number of input accounts")] #[test] @@ -551,4 +633,37 @@ mod tests { ] ); } + + #[test] + fn test_token_initialize_account_succeeds() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Definition ID with + data: vec![0; TOKEN_DEFINITION_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(1000)) + .collect(), + ..Account::default() + }, + is_authorized: false, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: AccountId::new([2; 32]), + }, + ]; + let post_states = initialize_account(&pre_states); + let [definition, holding] = post_states.try_into().ok().unwrap(); + assert_eq!(definition.data, pre_states[0].account.data); + assert_eq!( + holding.data, + vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + } } From c7bcd20a388bc20a09b26be67a0d12f5d91783fa Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 28 Nov 2025 11:37:49 -0300 Subject: [PATCH 54/90] add init function to the token program --- nssa/program_methods/guest/src/bin/token.rs | 137 ++++++++++++++++++-- 1 file changed, 126 insertions(+), 11 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index e5680be..821438a 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -3,7 +3,7 @@ use nssa_core::{ program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; -// The token program has two functions: +// The token program has three functions: // 1. New token definition. // Arguments to this function are: // * Two **default** accounts: [definition_account, holding_account]. @@ -18,6 +18,11 @@ use nssa_core::{ // * 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: +// * Two accounts: [definition_account, account_to_initialize]. +// * 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; @@ -45,6 +50,25 @@ impl TokenDefinition { bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); bytes.into() } + + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_DEFINITION_DATA_SIZE || data[0] != TOKEN_DEFINITION_TYPE { + None + } else { + let account_type = data[0]; + let name = data[1..7].try_into().unwrap(); + let total_supply = u128::from_le_bytes( + data[7..] + .try_into() + .expect("Total supply must be 16 bytes little-endian"), + ); + Some(Self { + account_type, + name, + total_supply, + }) + } + } } impl TokenHolding { @@ -61,8 +85,16 @@ impl TokenHolding { None } else { let account_type = data[0]; - let definition_id = AccountId::new(data[1..33].try_into().unwrap()); - let balance = u128::from_le_bytes(data[33..].try_into().unwrap()); + let definition_id = AccountId::new( + data[1..33] + .try_into() + .expect("Defintion ID must be 32 bytes long"), + ); + let balance = u128::from_le_bytes( + data[33..] + .try_into() + .expect("balance must be 16 bytes little-endian"), + ); Some(Self { definition_id, balance, @@ -167,6 +199,33 @@ fn new_definition( vec![definition_target_account_post, holding_target_account_post] } +fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of accounts"); + } + + let definition = &pre_states[0]; + let account_to_initialize = &pre_states[1]; + + if account_to_initialize.account != Account::default() { + panic!("Only uninitialized accounts can be initialized"); + } + + // TODO: We should check that this is an account owned by the token program. + // This check can't be done here since the ID of the program is known only after compiling it + // + // Check definition account is valid + let _definition_values = + TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); + let holding_values = TokenHolding::new(&definition.account_id); + + let definition_post = definition.account.clone(); + let mut account_to_initialize_post = account_to_initialize.account.clone(); + account_to_initialize_post.data = holding_values.into_data(); + + vec![definition_post, account_to_initialize_post] +} + type Instruction = [u8; 23]; fn main() { @@ -175,36 +234,59 @@ fn main() { instruction, } = read_nssa_inputs::(); - match instruction[0] { + let (pre_states, post_states) = match instruction[0] { 0 => { // Parse instruction - let total_supply = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); - let name: [u8; 6] = instruction[17..].try_into().unwrap(); + let total_supply = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("Total supply must be 16 bytes little-endian"), + ); + let name: [u8; 6] = instruction[17..] + .try_into() + .expect("Name must be 6 bytes long"); assert_ne!(name, [0; 6]); // Execute let post_states = new_definition(&pre_states, name, total_supply); - write_nssa_outputs(pre_states, post_states); + (pre_states, post_states) } 1 => { // Parse instruction - let balance_to_move = u128::from_le_bytes(instruction[1..17].try_into().unwrap()); - let name: [u8; 6] = instruction[17..].try_into().unwrap(); + let balance_to_move = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("Balance to move must be 16 bytes little-endian"), + ); + let name: [u8; 6] = instruction[17..] + .try_into() + .expect("Name must be 6 bytes long"); assert_eq!(name, [0; 6]); // Execute let post_states = transfer(&pre_states, balance_to_move); - write_nssa_outputs(pre_states, post_states); + (pre_states, post_states) + } + 2 => { + // Initialize account + assert_eq!(instruction[1..], [0; 22]); + let post_states = initialize_account(&pre_states); + (pre_states, post_states) } _ => panic!("Invalid instruction"), }; + + write_nssa_outputs(pre_states, post_states); } #[cfg(test)] mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; - use crate::{new_definition, transfer, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE}; + use crate::{ + TOKEN_DEFINITION_DATA_SIZE, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE, + initialize_account, new_definition, transfer, + }; #[should_panic(expected = "Invalid number of input accounts")] #[test] @@ -551,4 +633,37 @@ mod tests { ] ); } + + #[test] + fn test_token_initialize_account_succeeds() { + let pre_states = vec![ + AccountWithMetadata { + account: Account { + // Definition ID with + data: vec![0; TOKEN_DEFINITION_DATA_SIZE - 16] + .into_iter() + .chain(u128::to_le_bytes(1000)) + .collect(), + ..Account::default() + }, + is_authorized: false, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: AccountId::new([2; 32]), + }, + ]; + let post_states = initialize_account(&pre_states); + let [definition, holding] = post_states.try_into().ok().unwrap(); + assert_eq!(definition.data, pre_states[0].account.data); + assert_eq!( + holding.data, + vec![ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + } } From bfbd50e8cbe2ebf17efda6841dca98553cb75cfd Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 28 Nov 2025 17:09:38 -0300 Subject: [PATCH 55/90] add docs --- nssa/core/src/program.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index dfd52e4..1028d54 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -12,6 +12,11 @@ pub struct ProgramInput { pub instruction: T, } +/// A 32-byte seed used to compute a *Program-Derived AccountId* (PDA). +/// +/// Each program can derive up to `2^32` unique account IDs by choosing different +/// seeds. PDAs allow programs to control namespaced account identifiers without +/// collisions between programs. #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct PdaSeed([u8; 32]); From 7ab44507f50354226e58802af74ddc2084fc6453 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 28 Nov 2025 23:41:25 -0300 Subject: [PATCH 56/90] test --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3416efa..80e6366 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,18 @@ Run `wallet help` to check everything went well. ## Tutorial +This tutorial walks you through creating accounts and executing NSSA programs in both public and private contexts. + +> [!NOTE] +> The NSSA state is split into two separate but interconnected components: the public state and the private state. +> The public state is an on-chain, publicly visible record of accounts indexed by their Account IDs +> The private state mirrors this, but the actual account values are stored locally by each account owner. On-chain, only a hidden commitment to each private account state is recorded. This allows the chain to enforce freshness (i.e., prevent the reuse of stale private states) while preserving privacy and unlinkability across executions and private accounts. +> +> Every piece of state in NSSA is stored in an account (public or private). Accounts are either uninitialized or are owned by a program, and programs can only modify the accounts they own. +> +> In NSSA, accounts can only be modified through program execution. A program is the sole mechanism that can change an account’s value. +> Programs run publicly when all involved accounts are public, and privately when at least one private account participates. + ### Health-check Verify that the node is running and that the wallet can connect to it: @@ -175,7 +187,7 @@ Commands: ### Accounts -Every piece of state in NSSA is stored in an account. The CLI provides commands to manage accounts. Run `wallet account` to see the options available: +The CLI provides commands to manage accounts. Run `wallet account` to see the options available: ```bash Commands: get Get account data @@ -197,6 +209,11 @@ Generated new account with addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVuj This address is required when executing any program that interacts with the account. +> [!NOTE] +> Public accounts live on-chain and are identified by a 32-byte Account ID. +> Running `wallet account new public` generates a fresh keypair for the fixed signature scheme used in NSSA. +> The account ID is derived from the public key. The private key is used to sign transactions and to authorize the account in program executions. + #### Account initialization To query the account’s current status, run: From c937cb591e14fdf45c5d0737221464a9bbd7dd8c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 1 Dec 2025 10:24:44 -0300 Subject: [PATCH 57/90] wip --- README.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 80e6366..3fd04b9 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,9 @@ Commands: ### Accounts +> [!NOTE] +> Accounts are the basic unit of state in NSSA. They essentially hold native tokens and arbitrary data managed by some program. + The CLI provides commands to manage accounts. Run `wallet account` to see the options available: ```bash Commands: @@ -211,7 +214,7 @@ This address is required when executing any program that interacts with the acco > [!NOTE] > Public accounts live on-chain and are identified by a 32-byte Account ID. -> Running `wallet account new public` generates a fresh keypair for the fixed signature scheme used in NSSA. +> Running `wallet account new public` generates a fresh keypair for the signature scheme used in NSSA. > The account ID is derived from the public key. The private key is used to sign transactions and to authorize the account in program executions. #### Account initialization @@ -226,7 +229,10 @@ wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ Account is Uninitialized ``` -New accounts start as uninitialized, meaning no program owns them yet. Programs can claim uninitialized accounts; once claimed, the account becomes permanently owned by that program. +> [!NOTE] +> New accounts begin in an uninitialized state, meaning they are not yet owned by any program. A program may claim an uninitialized account; once claimed, the account becomes owned by that program. +> Owned accounts can only be modified through executions of the owning program. The only exception is native-token credits: any program may credit native tokens to any account. +> However, debiting native tokens from an account must always be performed by its owning program. In this example, we will initialize the account for the Authenticated transfer program, which securely manages native token transfers by requiring authentication for debits. @@ -288,7 +294,9 @@ wallet account new public Generated new account with addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS ``` -The new account is uninitialized. The authenticated transfers program will claim any uninitialized account used in a transfer. So we don't need to manually initialize the recipient account. + +> [!NOTE] +> The new account is uninitialized. The authenticated transfers program will claim any uninitialized account used in a transfer. So we don't need to manually initialize the recipient account. Let's send 37 tokens to the new account. @@ -321,6 +329,16 @@ Account owned by authenticated transfer program #### Create a new private account +> [!NOTE] +> Private accounts are structurally identical to public accounts; they differ only in how their state is stored off-chain and represented on-chain. +> The raw values of a private account are never stored on-chain. Instead, the chain only holds a 32-byte commitment (a hash-like binding to the actual values). Transactions include encrypted versions of the private values so that users can recover them from the blockchain. The decryption keys are known only to the user and are never shared. +> Private accounts are not managed through the usual signature mechanism used for public accounts. Instead, each private account is associated with two keypairs: +> - *Nullifier keys*, for using the corresponding private account in a private execution. +> - *Viewing keys*, used for encrypting and decrypting the values included in transactions. +> +> Private accounts also have a 32-byte identifier, derived from the nullifier public key. +> Just like public accounts, private accounts can only be initialized once. Any user can initialize them without knowing the owner's secret keys. However, modifying an initialized private account through an off-chain program execution requires knowledge of the owner’s secret keys. + Now let’s switch to the private state and create a private account. ```bash From ed38be57bbc56ee996e1cc21b4986bee69720733 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 1 Dec 2025 10:42:32 -0300 Subject: [PATCH 58/90] add explainers --- README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3fd04b9..d6d1a37 100644 --- a/README.md +++ b/README.md @@ -333,11 +333,15 @@ Account owned by authenticated transfer program > Private accounts are structurally identical to public accounts; they differ only in how their state is stored off-chain and represented on-chain. > The raw values of a private account are never stored on-chain. Instead, the chain only holds a 32-byte commitment (a hash-like binding to the actual values). Transactions include encrypted versions of the private values so that users can recover them from the blockchain. The decryption keys are known only to the user and are never shared. > Private accounts are not managed through the usual signature mechanism used for public accounts. Instead, each private account is associated with two keypairs: -> - *Nullifier keys*, for using the corresponding private account in a private execution. +> - *Nullifier keys*, for using the corresponding private account in privacy preserving executions. > - *Viewing keys*, used for encrypting and decrypting the values included in transactions. > > Private accounts also have a 32-byte identifier, derived from the nullifier public key. +> > Just like public accounts, private accounts can only be initialized once. Any user can initialize them without knowing the owner's secret keys. However, modifying an initialized private account through an off-chain program execution requires knowledge of the owner’s secret keys. +> +> Transactions that modify the values of a private account include a commitment to the new values, which will be added to the on-chain commitment set. They also include a nullifier that marks the previous version as old. +> The nullifier is constructed so that it cannot be linked to any prior commitment, ensuring that updates to the same private account cannot be correlated. Now let’s switch to the private state and create a private account. @@ -350,8 +354,8 @@ With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951 With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17 ``` -For now, focus only on the account address. Ignore the `npk` and `ipk` values. These are stored locally in the wallet and are used internally to build privacy-preserving transactions. -Also, the account id for private accounts is derived from the `npk` and `ipk` values. But we won't need them now. +For now, focus only on the account id. Ignore the `npk` and `ipk` values. These are the Nullifier public key and the Viewing public key. They are stored locally in the wallet and are used internally to build privacy-preserving transactions. +Also, the account id for private accounts is derived from the `npk` value. But we won't need them now. Just like public accounts, new private accounts start out uninitialized: @@ -401,8 +405,9 @@ Account owned by authenticated transfer program {"balance":17} ``` -Note: the last command does not query the network. -It works even offline because private account data lives only in your wallet storage. Other users cannot read your private balances. +> [!NOTE] +> The last command does not query the network. +> It works even offline because private account data lives only in your wallet storage. Other users cannot read your private balances. #### Digression: modifying private accounts @@ -448,6 +453,10 @@ We’ve shown how to use the authenticated-transfers program for transfers betwe ### The token program So far, we’ve made transfers using the authenticated-transfers program, which handles native token transfers. The Token program, on the other hand, is used for creating and managing custom tokens. + +> [!NOTE] +> The token program is a single program responsible for creating and managing all tokens. There is no need to deploy new programs to introduce new tokens. All token-related operations are performed by invoking the appropriate functions of the token program. + The CLI provides commands to execute the token program. To see the options available run `wallet token`: ```bash @@ -457,9 +466,11 @@ Commands: help Print this message or the help of the given subcommand(s) ``` -The Token program manages its accounts in two categories. Meaning, all accounts owned by the Token program fall into one of these types. -- Token definition accounts: these accounts store metadata about a token, such as its name, total supply, and other identifying properties. They act as the token’s unique identifier. -- Token holding accounts: these accounts hold actual token balances. In addition to the balance, they also record which token definition they belong to. + +> [!NOTE] +> The Token program manages its accounts in two categories. Meaning, all accounts owned by the Token program fall into one of these types. +> - Token definition accounts: these accounts store metadata about a token, such as its name, total supply, and other identifying properties. They act as the token’s unique identifier. +> - Token holding accounts: these accounts hold actual token balances. In addition to the balance, they also record which token definition they belong to. #### Creating a new token From 500e0862f3ea255747e4d38b06428676b024621c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 1 Dec 2025 10:48:43 -0300 Subject: [PATCH 59/90] s/address/account_id/ --- README.md | 76 +++++++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index d6d1a37..b74025b 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Nescience State Separation Architecture (NSSA) is a programmable blockchain syst ## Background -Typically, public blockchains maintain a fully transparent state, where the mapping from addresses to account values is entirely visible. In NSSA, we introduce a parallel *private state*, a new layer of accounts that coexists with the public one. The public and private states can be viewed as a partition of the address space: accounts with public addresses are openly visible, while private accounts are accessible only to holders of the corresponding viewing keys. Consistency across both states is enforced through zero-knowledge proofs (ZKPs). +Typically, public blockchains maintain a fully transparent state, where the mapping from account IDs to account values is entirely visible. In NSSA, we introduce a parallel *private state*, a new layer of accounts that coexists with the public one. The public and private states can be viewed as a partition of the account ID space: accounts with public IDs are openly visible, while private accounts are accessible only to holders of the corresponding viewing keys. Consistency across both states is enforced through zero-knowledge proofs (ZKPs). -Public accounts are represented on-chain as a visible map from addresses to account states and are modified in-place when their values change. Private accounts, by contrast, are never stored in raw form on-chain. Each update creates a new commitment, which cryptographically binds the current value of the account while preserving privacy. Commitments of previous valid versions remain on-chain, but a nullifier set is maintained to mark old versions as spent, ensuring that only the most up-to-date version of each private account can be used in any execution. +Public accounts are represented on-chain as a visible map from IDs to account states and are modified in-place when their values change. Private accounts, by contrast, are never stored in raw form on-chain. Each update creates a new commitment, which cryptographically binds the current value of the account while preserving privacy. Commitments of previous valid versions remain on-chain, but a nullifier set is maintained to mark old versions as spent, ensuring that only the most up-to-date version of each private account can be used in any execution. ### Programmability and selective privacy @@ -207,10 +207,10 @@ You can create both public and private accounts through the CLI. For example: wallet account new public # Output: -Generated new account with addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ +Generated new account with account_id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ ``` -This address is required when executing any program that interacts with the account. +This id is required when executing any program that interacts with the account. > [!NOTE] > Public accounts live on-chain and are identified by a 32-byte Account ID. @@ -222,8 +222,8 @@ This address is required when executing any program that interacts with the acco To query the account’s current status, run: ```bash -# Replace the address with yours -wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ +# Replace the id with yours +wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ # Output: Account is Uninitialized @@ -242,13 +242,13 @@ Initialize the account by running: # This command submits a public transaction executing the `init` function of the # Authenticated-transfer program. The wallet polls the sequencer until the # transaction is included in a block, which may take several seconds. -wallet auth-transfer init --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ +wallet auth-transfer init --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ ``` After it completes, check the updated account status: ```bash -wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ +wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ # Output: Account owned by authenticated transfer program @@ -260,14 +260,14 @@ Account owned by authenticated transfer program Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program. See the [Pinata](#piñata-program) section for instructions on how to use it. ```bash -# Complete with your address and the correct solution for your case -wallet pinata claim --to-addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ --solution 989106 +# Complete with your id and the correct solution for your case +wallet pinata claim --to-account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ --solution 989106 ``` After the claim succeeds, the account will be funded with some tokens: ```bash -wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ +wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ # Output: Account owned by authenticated transfer program @@ -291,7 +291,7 @@ Let's try it. For that we need to create another account for the recipient of th wallet account new public # Output: -Generated new account with addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS +Generated new account with account_id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS ``` @@ -311,7 +311,7 @@ Once that succeeds we can check the states. ```bash # Sender account -wallet account get --addr Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8 +wallet account get --account-id Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8 # Output: Account owned by authenticated transfer program @@ -320,7 +320,7 @@ Account owned by authenticated transfer program ```bash # Recipient account -wallet account get --addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS +wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS # Output: Account owned by authenticated transfer program @@ -349,7 +349,7 @@ Now let’s switch to the private state and create a private account. wallet account new private # Output: -Generated new account with addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL +Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951 With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17 ``` @@ -360,7 +360,7 @@ Also, the account id for private accounts is derived from the `npk` value. But w Just like public accounts, new private accounts start out uninitialized: ```bash -wallet account get --addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL +wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL # Output: Account is Uninitialized @@ -374,7 +374,7 @@ This happens because program execution logic does not depend on whether the invo Let’s send 17 tokens to the new private account. -The syntax is identical to the public-to-public transfer; just set the private address as the recipient. +The syntax is identical to the public-to-public transfer; just set the private ID as the recipient. This command will run the Authenticated-Transfer program locally, generate a proof, and submit it to the sequencer. Depending on your machine, this can take from 30 seconds to 4 minutes. @@ -389,7 +389,7 @@ After it succeeds, check both accounts: ```bash # Public sender account -wallet account get --addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS +wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS # Output: Account owned by authenticated transfer program @@ -398,7 +398,7 @@ Account owned by authenticated transfer program ```bash # Private recipient account -wallet account get --addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL +wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL # Output: Account owned by authenticated transfer program @@ -426,12 +426,12 @@ Let's create a new (uninitialized) private account like before: wallet account new private # Output: -Generated new account with addr Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5 +Generated new account with account_id Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5 With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e With ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 ``` -Now we'll ignore the private account address and focus on the `npk` and `ipk` values. We'll need this to send tokens to a foreign private account. Syntax is very similar. +Now we'll ignore the private account ID and focus on the `npk` and `ipk` values. We'll need this to send tokens to a foreign private account. Syntax is very similar. ```bash wallet auth-transfer send \ @@ -488,14 +488,14 @@ For example, let's create two new (uninitialized) public accounts and then use t wallet account new public # Output: -Generated new account with addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 +Generated new account with account_id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 ``` ```bash wallet account new public # Output: -Generated new account with addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw +Generated new account with account_id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw ``` Now we use them to create a new token. Let's call it the "Token A" @@ -504,14 +504,14 @@ Now we use them to create a new token. Let's call it the "Token A" wallet token new \ --name TOKENA \ --total-supply 1337 \ - --definition-addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 \ - --supply-addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw + --definition-account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 \ + --supply-account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw ``` After it succeeds, we can inspect the two accounts to see how they were initialized. ```bash -wallet account get --addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 +wallet account get --account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 # Output: Definition account owned by token program @@ -519,7 +519,7 @@ Definition account owned by token program ``` ```bash -wallet account get --addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw +wallet account get --account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw # Output: Holding account owned by token program @@ -536,7 +536,7 @@ Since we can’t reuse the accounts from the previous example, we need to create wallet account new public # Output: -Generated new account with addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii +Generated new account with account_id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii ``` ```bash @@ -544,7 +544,7 @@ wallet account new private # Output: -Generated new account with addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF +Generated new account with account_id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF With npk 6a2dfe433cf28e525aa0196d719be3c16146f7ee358ca39595323f94fde38f93 With ipk 03d59abf4bee974cc12ddb44641c19f0b5441fef39191f047c988c29a77252a577 ``` @@ -557,14 +557,14 @@ Now we use them to create a new token. Let's call it "Token B". wallet token new \ --name TOKENB \ --total-supply 7331 \ - --definition-addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii \ - --supply-addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF + --definition-account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii \ + --supply-account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF ``` After it succeeds, we can check their values ```bash -wallet account get --addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii +wallet account get --account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii # Output: Definition account owned by token program @@ -572,7 +572,7 @@ Definition account owned by token program ``` ```bash -wallet account get --addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF +wallet account get --account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF # Output: Holding account owned by token program @@ -593,7 +593,7 @@ Let's create a new public account for the recipient. wallet account new public # Output: -Generated new account with addr Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 +Generated new account with account_id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 ``` Let's send 10 B tokens to this new account. We'll debit this from the supply account used in the creation of the token. @@ -608,7 +608,7 @@ wallet token send \ Let's inspect the public account: ```bash -wallet account get --addr Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 +wallet account get --account-id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 # Output: Holding account owned by token program @@ -618,13 +618,13 @@ Holding account owned by token program ### Piñata program The testnet comes with a program that serves as a faucet for native tokens. We call it the Piñata. Use the command `wallet pinata claim` to get native tokens from it. This requires two parameters: -- `--to-addr` is the address of the account that will receive the tokens. **Use only initialized accounts here.** +- `--to-account-id` is the ID of the account that will receive the tokens. **Use only initialized accounts here.** - `--solution` a solution to the Pinata challenge. This will change every time the Pinata is successfully claimed. -To find the solution to the challenge, first query the Pinata account. This is always at the address: `Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7`. +To find the solution to the challenge, first query the Pinata account. This has always the ID: `Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7`. ```bash -wallet account get --addr Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7 +wallet account get --account-id Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7 # Output: {"balance":750,"program_owner_b64":"/SQ9PX+NYQgXm7YMP7VMUBRwvU/Bq4pHTTZcCpTC5FM=","data_b64":"A939OBnG9OhvzOocqfCAJKSYvtcuV15OHDIVNg34MC8i","nonce":0} From aee6f45a8b973027d9875fa3a3bcfad1c80c031c Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 27 Nov 2025 22:07:53 +0300 Subject: [PATCH 60/90] refactor: move some stuff to a more suitable place --- integration_tests/src/lib.rs | 2 +- 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 | 195 ++++++++++- 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 | 132 +++++++- wallet/src/lib.rs | 309 +----------------- wallet/src/main.rs | 24 +- wallet/src/program_interactions/mod.rs | 2 + .../pinata.rs} | 0 .../token.rs} | 0 17 files changed, 420 insertions(+), 398 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/lib.rs b/integration_tests/src/lib.rs index dc7188b..31cd177 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -56,7 +56,7 @@ fn make_private_account_input_from_str(account_id: &str) -> String { pub async fn pre_test( home_dir: PathBuf, ) -> Result<(ServerHandle, JoinHandle>, TempDir)> { - wallet::execute_setup("test_pass".to_owned()).await?; + wallet::cli::execute_setup("test_pass".to_owned()).await?; let home_dir_sequencer = home_dir.join("sequencer"); diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 55be519..e7ef4d1 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -16,13 +16,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::PersistentStorage, helperfunctions::{fetch_config, fetch_persistent_storage}, @@ -57,7 +59,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; @@ -92,7 +94,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, @@ -123,7 +125,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; @@ -162,7 +164,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()); @@ -203,7 +205,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; @@ -234,7 +236,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; @@ -289,7 +291,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition 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 { cci: ChainIndex::root(), }, @@ -302,7 +304,7 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder 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 { cci: ChainIndex::root(), }, @@ -315,7 +317,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 { cci: ChainIndex::root(), }, @@ -335,7 +337,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"); @@ -394,7 +396,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"); @@ -449,7 +451,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 { cci: ChainIndex::root(), }, @@ -462,7 +464,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 { cci: ChainIndex::root(), }, @@ -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 { cci: ChainIndex::root(), }, @@ -496,7 +498,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(); @@ -543,7 +545,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -577,7 +579,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -610,7 +612,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 { cci: ChainIndex::root(), }, @@ -623,7 +625,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 { cci: ChainIndex::root(), }, @@ -636,7 +638,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 { cci: ChainIndex::root(), }, @@ -657,7 +659,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(); @@ -711,7 +713,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 { @@ -723,7 +725,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) @@ -752,7 +754,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 { cci: ChainIndex::root(), }, @@ -765,7 +767,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 { cci: ChainIndex::root(), }, @@ -778,7 +780,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 { cci: ChainIndex::root(), }, @@ -799,7 +801,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(); @@ -836,7 +838,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -865,7 +867,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -894,7 +896,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 { cci: ChainIndex::root(), }, @@ -907,7 +909,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 { cci: ChainIndex::root(), }, @@ -920,7 +922,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 { cci: ChainIndex::root(), }, @@ -941,7 +943,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(); @@ -988,7 +990,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -1017,7 +1019,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - wallet::execute_subcommand(Command::Token(subcommand)) + wallet::cli::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -1049,7 +1051,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; @@ -1088,7 +1090,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"); }; @@ -1128,7 +1130,7 @@ pub fn prepare_function_map() -> HashMap { cci: ChainIndex::root(), })); - 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 @@ -1157,7 +1159,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"); }; @@ -1165,7 +1167,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(); @@ -1192,13 +1194,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 @@ -1228,7 +1230,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"); // }; @@ -1279,7 +1281,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; @@ -1322,7 +1324,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; @@ -1368,7 +1370,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"); }; @@ -1414,7 +1416,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; @@ -1493,7 +1495,7 @@ pub fn prepare_function_map() -> HashMap { cci: ChainIndex::root(), })); let SubcommandReturnValue::RegisterAccount { account_id } = - wallet::execute_subcommand(command).await.unwrap() + wallet::cli::execute_subcommand(command).await.unwrap() else { panic!("Error creating account"); }; @@ -1501,7 +1503,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(); @@ -1547,7 +1549,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"); }; @@ -1563,7 +1565,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(); @@ -1591,7 +1593,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 { cci: ChainIndex::root(), }, @@ -1617,7 +1619,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; @@ -1657,7 +1659,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(); @@ -1668,7 +1670,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 6e831d1..5b23b2b 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -7,10 +7,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..39c7874 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,15 +1,200 @@ -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), +} + +/// 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, + }, +} + +/// 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 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; + } +} + +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?; + + wallet_core.store_persistent_data().await?; + + Ok(()) +} 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 770d2bb..19d2d56 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -1,16 +1,21 @@ -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 key_protocol::key_protocol_core::NSSAUserData; -use nssa::Account; +use common::{ + block::HashableBlockData, sequencer_client::SequencerClient, transaction::NSSATransaction, +}; +use key_protocol::{ + key_management::key_tree::traits::KeyNode as _, key_protocol_core::NSSAUserData, +}; +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::{ InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig, @@ -225,6 +230,125 @@ 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.default_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 keys_node in wallet_core + .storage + .user_data + .private_key_tree + .key_map + .values() + { + let acc_account_id = keys_node.account_id(); + let key_chain = &keys_node.value.0; + + 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 6cb52fe..04ee92e 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -3,31 +3,20 @@ 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 key_protocol::key_management::key_tree::{chain_index::ChainIndex, traits::KeyNode}; +use key_protocol::key_management::key_tree::chain_index::ChainIndex; 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, }; @@ -37,9 +26,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; @@ -219,292 +207,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), -} - -/// 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, - }, -} - -/// 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.default_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 keys_node in wallet_core - .storage - .user_data - .private_key_tree - .key_map - .values() - { - let acc_account_id = keys_node.account_id(); - let key_chain = &keys_node.value.0; - - 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; - } -} - -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?; - - wallet_core.store_persistent_data().await?; - - Ok(()) -} diff --git a/wallet/src/main.rs b/wallet/src/main.rs index 304d788..7360d47 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, OverCommand, execute_continious_run, execute_setup, execute_subcommand}; +use wallet::cli::{Args, OverCommand, execute_continious_run, execute_setup, 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) @@ -24,19 +26,17 @@ fn main() -> Result<()> { if let Some(overcommand) = args.command { match overcommand { OverCommand::Command(command) => { - execute_subcommand(command).await.unwrap(); - } - OverCommand::Setup { password } => { - execute_setup(password).await.unwrap(); + let _output = execute_subcommand(command).await?; + Ok(()) } + OverCommand::Setup { password } => execute_setup(password).await, } } else if args.continious_run { - execute_continious_run().await.unwrap(); + execute_continious_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 df64f8864f85ed1c80ab2b21288abc6107f56f8a Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 01:19:06 +0300 Subject: [PATCH 61/90] 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 04ee92e..4cfbab3 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -4,19 +4,23 @@ 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 key_protocol::key_management::key_tree::chain_index::ChainIndex; 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, }; @@ -27,6 +31,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; @@ -144,6 +149,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 @@ -206,4 +220,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 55fc4e977770f51d96a9400f001093bfaa2bfdfb Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 01:57:59 +0300 Subject: [PATCH 62/90] 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 4cfbab3..f45bf93 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -32,8 +32,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 { @@ -224,9 +223,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?; @@ -241,7 +240,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 @@ -249,7 +248,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 0c44785a073d35da05e1694f4eba35c056d75afc Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 02:18:38 +0300 Subject: [PATCH 63/90] 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 f45bf93..f79d947 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -33,7 +33,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, @@ -224,12 +223,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() @@ -237,25 +248,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())) @@ -268,7 +279,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 4afbd65e3b3edb697815e7d1b47cce1cd2341bb3 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 02:37:43 +0300 Subject: [PATCH 64/90] 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 df88d8bad6b91c57e2503de92c7b0951c506ed3e Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Sun, 30 Nov 2025 03:16:47 +0300 Subject: [PATCH 65/90] feat: compute pinata solution in wallet --- integration_tests/src/test_suite_map.rs | 12 +-- wallet/Cargo.toml | 1 + wallet/src/cli/mod.rs | 4 +- .../src/cli/programs/native_token_transfer.rs | 16 ++-- wallet/src/cli/programs/pinata.rs | 92 ++++++++++++------- wallet/src/cli/programs/token.rs | 12 +-- wallet/src/main.rs | 10 +- 7 files changed, 86 insertions(+), 61 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index e7ef4d1..9903345 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1400,10 +1400,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(); @@ -1531,11 +1529,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(); @@ -1588,7 +1584,6 @@ pub fn prepare_function_map() -> HashMap { info!("########## test_pinata_private_receiver_new_account ##########"); 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 { @@ -1605,8 +1600,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/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/mod.rs b/wallet/src/cli/mod.rs index 39c7874..c1def06 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -81,7 +81,7 @@ pub enum OverCommand { pub struct Args { /// Continious run flag #[arg(short, long)] - pub continious_run: bool, + pub continuous_run: bool, /// Wallet command #[command(subcommand)] pub command: Option, @@ -162,7 +162,7 @@ pub async fn execute_subcommand(command: Command) -> Result Result<()> { +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?; 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..c0e2223 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 @@ -134,10 +123,6 @@ impl WalletSubcommand for PinataProgramSubcommandPublic { println!("Transaction data is {transfer_tx:?}"); - let path = wallet_core.store_persistent_data().await?; - - println!("Stored persistent accounts at {path:#?}"); - Ok(SubcommandReturnValue::Empty) } } @@ -153,16 +138,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 +192,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 diff --git a/wallet/src/main.rs b/wallet/src/main.rs index 7360d47..a8a4fbe 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -1,7 +1,7 @@ use anyhow::Result; use clap::{CommandFactory as _, Parser as _}; use tokio::runtime::Builder; -use wallet::cli::{Args, OverCommand, execute_continious_run, execute_setup, execute_subcommand}; +use wallet::cli::{Args, OverCommand, execute_continuous_run, execute_setup, execute_subcommand}; pub const NUM_THREADS: usize = 2; @@ -23,16 +23,16 @@ fn main() -> Result<()> { env_logger::init(); runtime.block_on(async move { - if let Some(overcommand) = args.command { - match overcommand { + if let Some(over_command) = args.command { + match over_command { OverCommand::Command(command) => { let _output = execute_subcommand(command).await?; Ok(()) } OverCommand::Setup { password } => execute_setup(password).await, } - } else if args.continious_run { - execute_continious_run().await + } else if args.continuous_run { + execute_continuous_run().await } else { let help = Args::command().render_long_help(); println!("{help}"); From 3d529d19fa82e5f37c88fd4cdc0f11c62b8875b1 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 2 Dec 2025 10:48:21 -0300 Subject: [PATCH 66/90] solve comments --- nssa/core/src/program.rs | 2 +- nssa/program_methods/guest/src/bin/token.rs | 39 ++++++++++----------- nssa/src/public_transaction/transaction.rs | 2 +- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 1028d54..b48a08b 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -14,7 +14,7 @@ pub struct ProgramInput { /// A 32-byte seed used to compute a *Program-Derived AccountId* (PDA). /// -/// Each program can derive up to `2^32` unique account IDs by choosing different +/// Each program can derive up to `2^256` unique account IDs by choosing different /// seeds. PDAs allow programs to control namespaced account identifiers without /// collisions between programs. #[derive(Serialize, Deserialize, Clone)] diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 0109c2b..b527045 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -82,25 +82,25 @@ impl TokenHolding { fn parse(data: &[u8]) -> Option { if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { - None - } else { - let account_type = data[0]; - let definition_id = AccountId::new( - data[1..33] - .try_into() - .expect("Defintion ID must be 32 bytes long"), - ); - let balance = u128::from_le_bytes( - data[33..] - .try_into() - .expect("balance must be 16 bytes little-endian"), - ); - Some(Self { - definition_id, - balance, - account_type, - }) + return None; } + + let account_type = data[0]; + let definition_id = AccountId::new( + data[1..33] + .try_into() + .expect("Defintion ID must be 32 bytes long"), + ); + let balance = u128::from_le_bytes( + data[33..] + .try_into() + .expect("balance must be 16 bytes little-endian"), + ); + Some(Self { + definition_id, + balance, + account_type, + }) } fn into_data(self) -> Data { @@ -211,7 +211,7 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { panic!("Only uninitialized accounts can be initialized"); } - // TODO: We should check that this is an account owned by the token program. + // TODO: #212 We should check that this is an account owned by the token program. // This check can't be done here since the ID of the program is known only after compiling it // // Check definition account is valid @@ -278,7 +278,6 @@ fn main() { write_nssa_outputs(pre_states, post_states); } - #[cfg(test)] mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index cafa27b..b8c0a8d 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -145,7 +145,7 @@ impl PublicTransaction { // authorized by program through the PDA mechanism let is_authorized = signer_account_ids.contains(&account_id) || authorized_pdas.contains(&account_id); - if pre.is_authorized && !is_authorized { + if pre.is_authorized != is_authorized { return Err(NssaError::InvalidProgramBehavior); } } From fdc53927ca5f2a33c8e81243ac2efa397f47a7e6 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy <41742639+schouhy@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:50:06 -0300 Subject: [PATCH 67/90] Update nssa/src/program.rs Co-authored-by: Daniil Polyakov --- nssa/src/program.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 4d2232d..f60237a 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -107,7 +107,7 @@ impl Program { pub fn pinata_token() -> Self { use crate::program_methods::PINATA_TOKEN_ELF; - Self::new(PINATA_TOKEN_ELF.to_vec()).unwrap() + Self::new(PINATA_TOKEN_ELF.to_vec()).expect("pinata token elf is defined in risc0 build of `program_methods`") } } From dcef017f9b95c839429fa77002dee6d862304fd9 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 2 Dec 2025 12:12:56 -0300 Subject: [PATCH 68/90] nit --- nssa/program_methods/guest/src/bin/token.rs | 2 +- nssa/src/program.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index b527045..71a4b8d 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -639,7 +639,7 @@ mod tests { AccountWithMetadata { account: Account { // Definition ID with - data: vec![0; TOKEN_DEFINITION_DATA_SIZE - 16] + data: [0; TOKEN_DEFINITION_DATA_SIZE - 16] .into_iter() .chain(u128::to_le_bytes(1000)) .collect(), diff --git a/nssa/src/program.rs b/nssa/src/program.rs index f60237a..b522c8a 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -107,7 +107,8 @@ impl Program { pub fn pinata_token() -> Self { use crate::program_methods::PINATA_TOKEN_ELF; - Self::new(PINATA_TOKEN_ELF.to_vec()).expect("pinata token elf is defined in risc0 build of `program_methods`") + Self::new(PINATA_TOKEN_ELF.to_vec()) + .expect("pinata token elf is defined in risc0 build of `program_methods`") } } From 407c3f3c9581667055993341a4353624058ec964 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 2 Dec 2025 16:27:22 -0300 Subject: [PATCH 69/90] improve expect message --- nssa/src/program.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nssa/src/program.rs b/nssa/src/program.rs index b522c8a..21a03ce 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -107,8 +107,7 @@ impl Program { pub fn pinata_token() -> Self { use crate::program_methods::PINATA_TOKEN_ELF; - Self::new(PINATA_TOKEN_ELF.to_vec()) - .expect("pinata token elf is defined in risc0 build of `program_methods`") + Self::new(PINATA_TOKEN_ELF.to_vec()).expect("Piñata program must be a valid R0BF file") } } From d16908d4631ba23c0fb1a1430e067182bb9b212c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 2 Dec 2025 16:48:12 -0300 Subject: [PATCH 70/90] remove pinata instructions --- README.md | 41 +++-------------------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b74025b..aa116e6 100644 --- a/README.md +++ b/README.md @@ -257,11 +257,11 @@ Account owned by authenticated transfer program ### Funding the account: executing the Piñata program -Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program. See the [Pinata](#piñata-program) section for instructions on how to use it. +Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program. ```bash -# Complete with your id and the correct solution for your case -wallet pinata claim --to-account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ --solution 989106 +# Complete with your id +wallet pinata claim --to Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ ``` After the claim succeeds, the account will be funded with some tokens: @@ -615,41 +615,6 @@ Holding account owned by token program {"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":10} ``` -### Piñata program - -The testnet comes with a program that serves as a faucet for native tokens. We call it the Piñata. Use the command `wallet pinata claim` to get native tokens from it. This requires two parameters: -- `--to-account-id` is the ID of the account that will receive the tokens. **Use only initialized accounts here.** -- `--solution` a solution to the Pinata challenge. This will change every time the Pinata is successfully claimed. - -To find the solution to the challenge, first query the Pinata account. This has always the ID: `Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7`. - -```bash -wallet account get --account-id Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7 - -# Output: -{"balance":750,"program_owner_b64":"/SQ9PX+NYQgXm7YMP7VMUBRwvU/Bq4pHTTZcCpTC5FM=","data_b64":"A939OBnG9OhvzOocqfCAJKSYvtcuV15OHDIVNg34MC8i","nonce":0} -``` - -Copy the `data_b64` value and run the following python script: - -```python -import base64, hashlib - -def find_16byte_prefix(data: str, max_attempts: int) -> bytes: - data = base64.b64decode(data_b64)[1:] - for attempt in range(max_attempts): - suffix = attempt.to_bytes(16, 'little') - h = hashlib.sha256(data + suffix).digest() - if h[:3] == b"\x00\x00\x00": - solution = int.from_bytes(suffix, 'little') - return f"Solution: {solution}" - raise RuntimeError(f"No suffix found in {max_attempts} attempts") - - -data_b64 = "A939OBnG9OhvzOocqfCAJKSYvtcuV15OHDIVNg34MC8i" # <- Change with the value from the Piñata account -print(find_16byte_prefix(data_b64, 50000000)) -``` - ### Chain information The wallet provides some commands to query information about the chain. These are under the `wallet chain-info` command. From 78ce57e19bc74d5321f590bc3d51812f59dda7a3 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 3 Dec 2025 13:50:10 +0200 Subject: [PATCH 71/90] fix: correct tokens names --- wallet/Cargo.toml | 1 + wallet/src/cli/account.rs | 56 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 3b12d8f..aeceb79 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -27,6 +27,7 @@ path = "../key_protocol" [dependencies.nssa] path = "../nssa" +features = ["no_docker"] [dependencies.common] path = "../common" diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 5b23b2b..f6bc90a 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -178,7 +178,17 @@ impl From for TokedDefinitionAccountView { fn from(value: TokenDefinition) -> Self { Self { account_type: "Token definition".to_string(), - name: hex::encode(value.name), + name: { + let mut name_vec_trim = vec![]; + for ch in value.name { + // Assuming, that name does not have UTF-8 NULL and all zeroes are padding. + if ch == 0 { + break; + } + name_vec_trim.push(ch); + } + String::from_utf8(name_vec_trim).unwrap_or(hex::encode(value.name)) + }, total_supply: value.total_supply, } } @@ -343,3 +353,47 @@ impl WalletSubcommand for AccountSubcommand { } } } + +#[cfg(test)] +mod tests { + use crate::cli::account::{TokedDefinitionAccountView, TokenDefinition}; + + #[test] + fn test_invalid_utf_8_name_of_token() { + let token_def = TokenDefinition { + account_type: 1, + name: [137, 12, 14, 3, 5, 4], + total_supply: 100, + }; + + let token_def_view: TokedDefinitionAccountView = token_def.into(); + + assert_eq!(token_def_view.name, "890c0e030504"); + } + + #[test] + fn test_valid_utf_8_name_of_token_all_bytes() { + let token_def = TokenDefinition { + account_type: 1, + name: [240, 159, 146, 150, 66, 66], + total_supply: 100, + }; + + let token_def_view: TokedDefinitionAccountView = token_def.into(); + + assert_eq!(token_def_view.name, "💖BB"); + } + + #[test] + fn test_valid_utf_8_name_of_token_less_bytes() { + let token_def = TokenDefinition { + account_type: 1, + name: [78, 65, 77, 69, 0, 0], + total_supply: 100, + }; + + let token_def_view: TokedDefinitionAccountView = token_def.into(); + + assert_eq!(token_def_view.name, "NAME"); + } +} From 282b932a8e33f16785814ffa4ccdca4b32478f0e Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Wed, 3 Dec 2025 14:30:23 +0200 Subject: [PATCH 72/90] fix: correct feature --- wallet/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index aeceb79..3b12d8f 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -27,7 +27,6 @@ path = "../key_protocol" [dependencies.nssa] path = "../nssa" -features = ["no_docker"] [dependencies.common] path = "../common" From 91c898f19cb595ec6b6c75c65aed222fecb721ce Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Wed, 3 Dec 2025 00:17:12 +0300 Subject: [PATCH 73/90] refactor: split block polling --- wallet/Cargo.toml | 2 + wallet/src/cli/account.rs | 13 +--- wallet/src/cli/mod.rs | 26 ++----- wallet/src/helperfunctions.rs | 132 ++-------------------------------- wallet/src/lib.rs | 94 +++++++++++++++++++++++- wallet/src/poller.rs | 16 ++++- 6 files changed, 122 insertions(+), 161 deletions(-) diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 3b12d8f..34fc84c 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -21,6 +21,8 @@ hex = "0.4.3" rand.workspace = true itertools = "0.14.0" sha2.workspace = true +futures.workspace = true +async-stream = "0.3.6" [dependencies.key_protocol] path = "../key_protocol" diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 5b23b2b..aeaf182 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -9,9 +9,7 @@ use serde::Serialize; use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, - helperfunctions::{ - AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix, parse_block_range, - }, + helperfunctions::{AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix}, }; const TOKEN_DEFINITION_TYPE: u8 = 0; @@ -278,7 +276,6 @@ impl WalletSubcommand for AccountSubcommand { new_subcommand.handle_subcommand(wallet_core).await } AccountSubcommand::SyncPrivate {} => { - let last_synced_block = wallet_core.last_synced_block; let curr_last_block = wallet_core .sequencer_client .get_last_block() @@ -298,13 +295,7 @@ impl WalletSubcommand for AccountSubcommand { println!("Stored persistent data at {path:#?}"); } else { - parse_block_range( - last_synced_block + 1, - curr_last_block, - wallet_core.sequencer_client.clone(), - wallet_core, - ) - .await?; + wallet_core.sync_to_block(curr_last_block).await?; } Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block)) diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index c1def06..eb4e891 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,8 +1,5 @@ -use std::sync::Arc; - use anyhow::Result; use clap::{Parser, Subcommand}; -use common::sequencer_client::SequencerClient; use nssa::program::Program; use crate::{ @@ -16,7 +13,7 @@ use crate::{ token::TokenProgramAgnosticSubcommand, }, }, - helperfunctions::{fetch_config, parse_block_range}, + helperfunctions::fetch_config, }; pub mod account; @@ -164,29 +161,20 @@ pub async fn execute_subcommand(command: Command) -> Result 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; + let latest_block_num = wallet_core + .sequencer_client + .get_last_block() + .await? + .last_block; + wallet_core.sync_to_block(latest_block_num).await?; 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/helperfunctions.rs b/wallet/src/helperfunctions.rs index 19d2d56..770d2bb 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -1,21 +1,16 @@ -use std::{path::PathBuf, str::FromStr, sync::Arc}; +use std::{path::PathBuf, str::FromStr}; 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_management::key_tree::traits::KeyNode as _, key_protocol_core::NSSAUserData, -}; -use nssa::{Account, privacy_preserving_transaction::message::EncryptedAccountData}; +use key_protocol::key_protocol_core::NSSAUserData; +use nssa::Account; use nssa_core::account::Nonce; use rand::{RngCore, rngs::OsRng}; use serde::Serialize; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ - HOME_DIR_ENV_VAR, WalletCore, + HOME_DIR_ENV_VAR, config::{ InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig, @@ -230,125 +225,6 @@ 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.default_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 keys_node in wallet_core - .storage - .user_data - .private_key_tree - .key_map - .values() - { - let acc_account_id = keys_node.account_id(); - let key_chain = &keys_node.value.0; - - 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 f79d947..2886dcd 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -9,9 +9,12 @@ use common::{ transaction::{EncodedTransaction, NSSATransaction}, }; use config::WalletConfig; -use key_protocol::key_management::key_tree::chain_index::ChainIndex; +use key_protocol::key_management::key_tree::{chain_index::ChainIndex, traits::KeyNode as _}; use log::info; -use nssa::{Account, AccountId, PrivacyPreservingTransaction, program::Program}; +use nssa::{ + Account, AccountId, PrivacyPreservingTransaction, + privacy_preserving_transaction::message::EncryptedAccountData, program::Program, +}; use nssa_core::{Commitment, MembershipProof, SharedSecretKey, program::InstructionData}; pub use privacy_preserving_tx::PrivacyPreservingAccount; use tokio::io::AsyncWriteExt; @@ -293,4 +296,91 @@ impl WalletCore { shared_secrets, )) } + + pub async fn sync_to_block(&mut self, block_id: u64) -> Result<()> { + use futures::TryStreamExt as _; + + if self.last_synced_block >= block_id { + return Ok(()); + } + + let poller = self.poller.clone(); + let mut blocks = + std::pin::pin!(poller.poll_block_range(self.last_synced_block + 1..=block_id)); + + while let Some(block) = blocks.try_next().await? { + for tx in block.transactions { + let nssa_tx = NSSATransaction::try_from(&tx)?; + self.sync_private_accounts_with_tx(nssa_tx); + } + + self.last_synced_block = block.block_id; + self.store_persistent_data().await?; + + println!( + "Block at id {} with timestamp {} parsed", + block.block_id, block.timestamp, + ); + } + + Ok(()) + } + + fn sync_private_accounts_with_tx(&mut self, tx: NSSATransaction) { + let NSSATransaction::PrivacyPreserving(tx) = tx else { + return; + }; + + let private_account_key_chains = self + .storage + .user_data + .default_user_private_accounts + .iter() + .map(|(acc_account_id, (key_chain, _))| (*acc_account_id, key_chain)) + .chain( + self.storage + .user_data + .private_key_tree + .key_map + .values() + .map(|keys_node| (keys_node.account_id(), &keys_node.value.0)), + ); + + let affected_accounts = private_account_key_chains + .flat_map(|(acc_account_id, key_chain)| { + let view_tag = EncryptedAccountData::compute_view_tag( + key_chain.nullifer_public_key.clone(), + key_chain.incoming_viewing_public_key.clone(), + ); + + tx.message() + .encrypted_private_post_states + .iter() + .enumerate() + .filter(move |(_, encrypted_data)| encrypted_data.view_tag == view_tag) + .filter_map(|(ciph_id, encrypted_data)| { + 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()); + + nssa_core::EncryptionScheme::decrypt( + ciphertext, + &shared_secret, + commitment, + ciph_id as u32, + ) + }) + .map(move |res_acc| (acc_account_id, res_acc)) + }) + .collect::>(); + + for (affected_account_id, new_acc) in affected_accounts { + println!( + "Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}" + ); + self.storage + .insert_private_account_data(affected_account_id, new_acc); + } + } } diff --git a/wallet/src/poller.rs b/wallet/src/poller.rs index 2b709e7..0e2192d 100644 --- a/wallet/src/poller.rs +++ b/wallet/src/poller.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use common::sequencer_client::SequencerClient; +use common::{block::HashableBlockData, sequencer_client::SequencerClient}; use log::{info, warn}; use crate::config::WalletConfig; @@ -66,4 +66,18 @@ impl TxPoller { anyhow::bail!("Transaction not found in preconfigured amount of blocks"); } + + pub fn poll_block_range( + &self, + range: std::ops::RangeInclusive, + ) -> impl futures::Stream> { + async_stream::stream! { + for block_id in range { + let block = borsh::from_slice::( + &self.client.get_block(block_id).await?.block, + )?; + yield Ok(block); + } + } + } } From d677db7f4e5170432f302442e1cc8c74d90a37e0 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 2 Dec 2025 17:12:32 -0300 Subject: [PATCH 74/90] add account post state struct with claiming request field --- nssa/core/src/program.rs | 68 ++++++++++++++++--- .../guest/src/bin/authenticated_transfer.rs | 11 +-- nssa/program_methods/guest/src/bin/pinata.rs | 2 +- .../src/bin/privacy_preserving_circuit.rs | 4 +- nssa/program_methods/guest/src/bin/token.rs | 26 +++---- nssa/src/program.rs | 4 +- nssa/src/public_transaction/transaction.rs | 6 +- .../guest/src/bin/burner.rs | 2 +- .../guest/src/bin/chain_caller.rs | 2 +- .../guest/src/bin/data_changer.rs | 2 +- .../guest/src/bin/extra_output.rs | 2 +- .../guest/src/bin/minter.rs | 2 +- .../guest/src/bin/missing_output.rs | 2 +- .../guest/src/bin/nonce_changer.rs | 2 +- .../guest/src/bin/program_owner_changer.rs | 2 +- .../guest/src/bin/simple_balance_transfer.rs | 4 +- 16 files changed, 96 insertions(+), 45 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 054f993..c79a841 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -20,11 +20,34 @@ pub struct ChainedCall { pub pre_states: Vec, } +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +pub struct AccountPostState { + pub account: Account, + pub claim: bool, +} + +impl From for AccountPostState { + fn from(account: Account) -> Self { + AccountPostState { + account, + claim: false, + } + } +} + +impl AccountPostState { + pub fn with_claim_request(mut self) -> Self { + self.claim = true; + self + } +} + #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ProgramOutput { pub pre_states: Vec, - pub post_states: Vec, + pub post_states: Vec, pub chained_calls: Vec, } @@ -38,7 +61,10 @@ pub fn read_nssa_inputs() -> ProgramInput { } } -pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec) { +pub fn write_nssa_outputs( + pre_states: Vec, + post_states: Vec, +) { let output = ProgramOutput { pre_states, post_states, @@ -49,7 +75,7 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec pub fn write_nssa_outputs_with_chained_call( pre_states: Vec, - post_states: Vec, + post_states: Vec, chained_calls: Vec, ) { let output = ProgramOutput { @@ -68,7 +94,7 @@ pub fn write_nssa_outputs_with_chained_call( /// - `executing_program_id`: The identifier of the program that was executed. pub fn validate_execution( pre_states: &[AccountWithMetadata], - post_states: &[Account], + post_states: &[AccountPostState], executing_program_id: ProgramId, ) -> bool { // 1. Lengths must match @@ -78,25 +104,27 @@ pub fn validate_execution( for (pre, post) in pre_states.iter().zip(post_states) { // 2. Nonce must remain unchanged - if pre.account.nonce != post.nonce { + if pre.account.nonce != post.account.nonce { return false; } // 3. Program ownership changes are not allowed - if pre.account.program_owner != post.program_owner { + if pre.account.program_owner != post.account.program_owner { return false; } let account_program_owner = pre.account.program_owner; // 4. Decreasing balance only allowed if owned by executing program - if post.balance < pre.account.balance && account_program_owner != executing_program_id { + if post.account.balance < pre.account.balance + && account_program_owner != executing_program_id + { return false; } // 5. Data changes only allowed if owned by executing program or if account pre state has // default values - if pre.account.data != post.data + if pre.account.data != post.account.data && pre.account != Account::default() && account_program_owner != executing_program_id { @@ -105,17 +133,37 @@ pub fn validate_execution( // 6. If a post state has default program owner, the pre state must have been a default // account - if post.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { + if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { return false; } } // 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.balance).sum(); + let total_balance_post_states: u128 = post_states.iter().map(|post| post.account.balance).sum(); if total_balance_pre_states != total_balance_post_states { return false; } true } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_account_post_state_from_account_constructor() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef], + nonce: 10, + }; + + let account_post_state: AccountPostState = account.clone().into(); + + assert_eq!(account, account_post_state.account); + assert!(!account_post_state.claim); + } +} diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index df8a38e..14aded4 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,16 +1,16 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, - program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, + 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 account_to_claim: AccountPostState = pre_state.account.clone().into(); let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values - if account_to_claim != Account::default() { + if account_to_claim.account != Account::default() { return; } @@ -41,7 +41,10 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance sender_post.balance -= balance_to_move; recipient_post.balance += balance_to_move; - write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); + write_nssa_outputs( + vec![sender, recipient], + vec![sender_post.into(), recipient_post.into()], + ); } /// A transfer of balance program. diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index fbea167..9337ab7 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -66,5 +66,5 @@ fn main() { pinata_post.data = data.next_data().to_vec(); winner_post.balance += PRIZE; - write_nssa_outputs(vec![pinata, winner], vec![pinata_post, winner_post]); + write_nssa_outputs(vec![pinata, winner], vec![pinata_post.into(), winner_post.into()]); } diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 6696245..e822f88 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -70,7 +70,7 @@ fn main() { // Public account public_pre_states.push(pre_states[i].clone()); - let mut post = post_states[i].clone(); + let mut post = post_states[i].account.clone(); if pre_states[i].is_authorized { post.nonce += 1; } @@ -126,7 +126,7 @@ fn main() { } // Update post-state with new nonce - let mut post_with_updated_values = post_states[i].clone(); + let mut post_with_updated_values = post_states[i].account.clone(); post_with_updated_values.nonce = *new_nonce; if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID { diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 821438a..1e9cc80 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -1,6 +1,6 @@ use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Data}, - program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, + program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput}, }; // The token program has three functions: @@ -112,7 +112,7 @@ impl TokenHolding { } } -fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec { +fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec { if pre_states.len() != 2 { panic!("Invalid number of input accounts"); } @@ -156,14 +156,14 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec Vec { +) -> Vec { if pre_states.len() != 2 { panic!("Invalid number of input accounts"); } @@ -196,10 +196,10 @@ fn new_definition( let mut holding_target_account_post = holding_target_account.account.clone(); holding_target_account_post.data = token_holding.into_data(); - vec![definition_target_account_post, holding_target_account_post] + vec![definition_target_account_post.into(), holding_target_account_post.into()] } -fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { +fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { if pre_states.len() != 2 { panic!("Invalid number of accounts"); } @@ -223,7 +223,7 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { let mut account_to_initialize_post = account_to_initialize.account.clone(); account_to_initialize_post.data = holding_values.into_data(); - vec![definition_post, account_to_initialize_post] + vec![definition_post.into(), account_to_initialize_post.into()] } type Instruction = [u8; 23]; @@ -387,14 +387,14 @@ 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.data, + definition_account.account.data, vec![ 0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); assert_eq!( - holding_account.data, + holding_account.account.data, vec![ 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -619,14 +619,14 @@ mod tests { let post_states = transfer(&pre_states, 11); let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); assert_eq!( - sender_post.data, + sender_post.account.data, vec![ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); assert_eq!( - recipient_post.data, + recipient_post.account.data, vec![ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 @@ -657,9 +657,9 @@ mod tests { ]; let post_states = initialize_account(&pre_states); let [definition, holding] = post_states.try_into().ok().unwrap(); - assert_eq!(definition.data, pre_states[0].account.data); + assert_eq!(definition.account.data, pre_states[0].account.data); assert_eq!( - holding.data, + holding.account.data, vec![ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 diff --git a/nssa/src/program.rs b/nssa/src/program.rs index d3f28b5..cf5334c 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -239,8 +239,8 @@ mod tests { let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap(); - assert_eq!(sender_post, expected_sender_post); - assert_eq!(recipient_post, expected_recipient_post); + assert_eq!(sender_post.account, expected_sender_post); + assert_eq!(recipient_post.account, expected_recipient_post); } #[test] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 28f33fb..560c8b3 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -155,8 +155,8 @@ impl PublicTransaction { // The invoked program claims the accounts with default program id. for post in program_output.post_states.iter_mut() { - if post.program_owner == DEFAULT_PROGRAM_ID { - post.program_owner = chained_call.program_id; + if post.account.program_owner == DEFAULT_PROGRAM_ID { + post.account.program_owner = chained_call.program_id; } } @@ -166,7 +166,7 @@ impl PublicTransaction { .iter() .zip(program_output.post_states.iter()) { - state_diff.insert(pre.account_id, post.clone()); + state_diff.insert(pre.account_id, post.account.clone()); } for new_call in program_output.chained_calls.into_iter().rev() { diff --git a/nssa/test_program_methods/guest/src/bin/burner.rs b/nssa/test_program_methods/guest/src/bin/burner.rs index 1ef7373..812a1a0 100644 --- a/nssa/test_program_methods/guest/src/bin/burner.rs +++ b/nssa/test_program_methods/guest/src/bin/burner.rs @@ -17,5 +17,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance -= balance_to_burn; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post.into()]); } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index 028f8a0..4fecc40 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -37,7 +37,7 @@ fn main() { write_nssa_outputs_with_chained_call( vec![sender_pre.clone(), receiver_pre.clone()], - vec![sender_pre.account, receiver_pre.account], + vec![sender_pre.account.into(), receiver_pre.account.into()], chained_call, ); } diff --git a/nssa/test_program_methods/guest/src/bin/data_changer.rs b/nssa/test_program_methods/guest/src/bin/data_changer.rs index c7d34a2..da9bf25 100644 --- a/nssa/test_program_methods/guest/src/bin/data_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/data_changer.rs @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.data.push(0); - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post.into()]); } diff --git a/nssa/test_program_methods/guest/src/bin/extra_output.rs b/nssa/test_program_methods/guest/src/bin/extra_output.rs index 3543d51..cf6543a 100644 --- a/nssa/test_program_methods/guest/src/bin/extra_output.rs +++ b/nssa/test_program_methods/guest/src/bin/extra_output.rs @@ -15,5 +15,5 @@ fn main() { let account_pre = pre.account.clone(); - write_nssa_outputs(vec![pre], vec![account_pre, Account::default()]); + write_nssa_outputs(vec![pre], vec![account_pre.into(), Account::default().into()]); } diff --git a/nssa/test_program_methods/guest/src/bin/minter.rs b/nssa/test_program_methods/guest/src/bin/minter.rs index 2ec97a9..2d8683e 100644 --- a/nssa/test_program_methods/guest/src/bin/minter.rs +++ b/nssa/test_program_methods/guest/src/bin/minter.rs @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance += 1; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post.into()]); } diff --git a/nssa/test_program_methods/guest/src/bin/missing_output.rs b/nssa/test_program_methods/guest/src/bin/missing_output.rs index 7b6016c..1609759 100644 --- a/nssa/test_program_methods/guest/src/bin/missing_output.rs +++ b/nssa/test_program_methods/guest/src/bin/missing_output.rs @@ -12,5 +12,5 @@ fn main() { let account_pre1 = pre1.account.clone(); - write_nssa_outputs(vec![pre1, pre2], vec![account_pre1]); + write_nssa_outputs(vec![pre1, pre2], vec![account_pre1.into()]); } diff --git a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs index b3b2599..c8e3485 100644 --- a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.nonce += 1; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post.into()]); } diff --git a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs index 49947cd..b5b74e0 100644 --- a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7]; - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(vec![pre], vec![account_post.into()]); } diff --git a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs index 13263c5..d057c07 100644 --- a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs +++ b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; type Instruction = u128; @@ -20,6 +20,6 @@ fn main() { write_nssa_outputs( vec![sender_pre, receiver_pre], - vec![sender_post, receiver_post], + vec![sender_post.into(), receiver_post.into()], ); } From 8a269858c5b171b39aec20159143f45af2a6f720 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 3 Dec 2025 16:39:33 -0300 Subject: [PATCH 75/90] improve struct interface --- nssa/core/src/program.rs | 40 +++++++++++++++---- .../guest/src/bin/authenticated_transfer.rs | 34 +++++++++++----- nssa/program_methods/guest/src/bin/pinata.rs | 10 ++++- nssa/program_methods/guest/src/bin/token.rs | 31 ++++++++++---- nssa/src/public_transaction/transaction.rs | 8 +++- .../guest/src/bin/burner.rs | 4 +- .../guest/src/bin/chain_caller.rs | 8 +++- .../guest/src/bin/data_changer.rs | 4 +- .../guest/src/bin/extra_output.rs | 10 ++++- .../guest/src/bin/minter.rs | 4 +- .../guest/src/bin/missing_output.rs | 4 +- .../guest/src/bin/nonce_changer.rs | 4 +- .../guest/src/bin/program_owner_changer.rs | 4 +- .../guest/src/bin/simple_balance_transfer.rs | 7 +++- 14 files changed, 126 insertions(+), 46 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index c79a841..8874773 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -24,16 +24,26 @@ pub struct ChainedCall { #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct AccountPostState { pub account: Account, - pub claim: bool, + claim: bool, } -impl From for AccountPostState { - fn from(account: Account) -> Self { - AccountPostState { +impl AccountPostState { + pub fn new(account: Account) -> Self { + Self { account, claim: false, } } + pub fn new_claimed(account: Account) -> Self { + Self { + account, + claim: true, + } + } + + pub fn requires_claim(&self) -> bool { + self.claim + } } impl AccountPostState { @@ -153,7 +163,7 @@ mod tests { use super::*; #[test] - fn test_account_post_state_from_account_constructor() { + fn test_post_state_new_without_claim_constructor() { let account = Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 1337, @@ -161,9 +171,25 @@ mod tests { nonce: 10, }; - let account_post_state: AccountPostState = account.clone().into(); + let account_post_state = AccountPostState::new_claimed(account.clone()); assert_eq!(account, account_post_state.account); - assert!(!account_post_state.claim); + assert!(account_post_state.requires_claim()); } + + #[test] + fn test_post_state_new_with_claim_constructor() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef], + nonce: 10, + }; + + let account_post_state = AccountPostState::new(account.clone()); + + assert_eq!(account, account_post_state.account); + assert!(!account_post_state.requires_claim()); + } + } diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index 14aded4..f711f20 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,12 +1,14 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, - program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}, + program::{ + AccountPostState, DEFAULT_PROGRAM_ID, 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: AccountPostState = pre_state.account.clone().into(); + let account_to_claim = AccountPostState::new_claimed(pre_state.account.clone()); let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values @@ -36,15 +38,27 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance } // 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; - recipient_post.balance += balance_to_move; + let sender_post: AccountPostState = { + // Modify sender's balance + let mut sender_post_account = sender.account.clone(); + sender_post_account.balance -= balance_to_move; + AccountPostState::new(sender_post_account) + }; - write_nssa_outputs( - vec![sender, recipient], - vec![sender_post.into(), recipient_post.into()], - ); + let recipient_post = { + // Modify recipient's balance + let mut recipient_post_account = recipient.account.clone(); + recipient_post_account.balance += balance_to_move; + + // Claim recipient account if it has default program owner + if recipient_post_account.program_owner == DEFAULT_PROGRAM_ID { + AccountPostState::new_claimed(recipient_post_account) + } else { + AccountPostState::new(recipient_post_account) + } + }; + + write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); } /// A transfer of balance program. diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index 9337ab7..50aac7b 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; use risc0_zkvm::sha::{Impl, Sha256}; const PRIZE: u128 = 150; @@ -66,5 +66,11 @@ fn main() { pinata_post.data = data.next_data().to_vec(); winner_post.balance += PRIZE; - write_nssa_outputs(vec![pinata, winner], vec![pinata_post.into(), winner_post.into()]); + write_nssa_outputs( + vec![pinata, winner], + vec![ + AccountPostState::new(pinata_post), + AccountPostState::new(winner_post), + ], + ); } diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 1e9cc80..ce4558a 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -1,6 +1,8 @@ use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Data}, - program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput}, + program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, + }, }; // The token program has three functions: @@ -148,15 +150,22 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec Vec { @@ -220,10 +232,13 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec Date: Wed, 3 Dec 2025 17:06:09 -0300 Subject: [PATCH 76/90] add test --- nssa/core/src/program.rs | 2 +- nssa/src/state.rs | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 8874773..744c1dc 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -34,6 +34,7 @@ impl AccountPostState { claim: false, } } + pub fn new_claimed(account: Account) -> Self { Self { account, @@ -191,5 +192,4 @@ mod tests { assert_eq!(account, account_post_state.account); assert!(!account_post_state.requires_claim()); } - } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index cef7791..4c3f8ac 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2162,4 +2162,56 @@ pub mod tests { Err(NssaError::MaxChainedCallsDepthExceeded) )); } + + #[test] + fn test_claiming_mechanism_within_chain_call() { + // This test calls the authenticated transfer program through the chain_caller program. + // The transfer is made from an initialized sender to an uninitialized recipient. And + // it is expected that the recipient account is claimed by the authenticated transfer + // program and not the chained_caller program. + let chain_caller = Program::chain_caller(); + let auth_transfer = Program::authenticated_transfer_program(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let initial_balance = 100; + let initial_data = [(account_id, initial_balance)]; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let from = account_id; + let from_key = key; + let to = AccountId::new([2; 32]); + let amount: u128 = 37; + + // Check the recipient is an uninitialized account + assert_eq!(state.get_account_by_id(&to), Account::default()); + + let expected_to_post = Account { + // The expected program owner is the authenticated transfer program + program_owner: auth_transfer.id(), + balance: amount, + ..Account::default() + }; + + // The transaction executes the chain_caller program, which internally calls the + // authenticated_transfer program + let instruction: (u128, ProgramId, u32) = + (amount, Program::authenticated_transfer_program().id(), 1); + let message = public_transaction::Message::try_new( + chain_caller.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![0], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx).unwrap(); + + let from_post = state.get_account_by_id(&from); + let to_post = state.get_account_by_id(&to); + assert_eq!(from_post.balance, initial_balance - amount); + assert_eq!(to_post, expected_to_post); + } } From 44b4c53d046db163f65ef028a2460ee3dda2f5d3 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 3 Dec 2025 17:36:53 -0300 Subject: [PATCH 77/90] add test that initialized accounts cannot be claimed --- nssa/src/program.rs | 9 +++++++ nssa/src/state.rs | 27 +++++++++++++++++++ .../guest/src/bin/claimer.rs | 19 +++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 nssa/test_program_methods/guest/src/bin/claimer.rs diff --git a/nssa/src/program.rs b/nssa/src/program.rs index cf5334c..91328b5 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -207,6 +207,15 @@ mod tests { elf: CHAIN_CALLER_ELF.to_vec(), } } + + pub fn claimer() -> Self { + use test_program_methods::{CLAIMER_ELF, CLAIMER_ID}; + + Program { + id: CLAIMER_ID, + elf: CLAIMER_ELF.to_vec(), + } + } } #[test] diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 4c3f8ac..026e2a9 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -477,6 +477,7 @@ pub mod tests { self.insert_program(Program::minter()); self.insert_program(Program::burner()); self.insert_program(Program::chain_caller()); + self.insert_program(Program::claimer()); self } @@ -2214,4 +2215,30 @@ pub mod tests { assert_eq!(from_post.balance, initial_balance - amount); assert_eq!(to_post, expected_to_post); } + + #[test] + fn test_claiming_mechanism_cannot_claim_initialied_accounts() { + let claimer = Program::claimer(); + let mut state = V02State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let account_id = AccountId::new([2; 32]); + + // Insert an account with non-default program owner + state.force_insert_account( + account_id, + Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + ); + + let message = + public_transaction::Message::try_new(claimer.id(), vec![account_id], vec![], ()) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx); + + assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))) + } } diff --git a/nssa/test_program_methods/guest/src/bin/claimer.rs b/nssa/test_program_methods/guest/src/bin/claimer.rs new file mode 100644 index 0000000..7687e5a --- /dev/null +++ b/nssa/test_program_methods/guest/src/bin/claimer.rs @@ -0,0 +1,19 @@ +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; + +type Instruction = (); + +fn main() { + let ProgramInput { + pre_states, + instruction: _, + } = read_nssa_inputs::(); + + let [pre] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let account_post = AccountPostState::new_claimed(pre.account.clone()); + + write_nssa_outputs(vec![pre], vec![account_post]); +} From ed949c07b18d725703054f4b1fc7c0d23585b17d Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 3 Dec 2025 17:38:45 -0300 Subject: [PATCH 78/90] nit --- nssa/program_methods/guest/src/bin/authenticated_transfer.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index f711f20..c9fc10b 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -6,7 +6,6 @@ use nssa_core::{ }; /// 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 = AccountPostState::new_claimed(pre_state.account.clone()); let is_authorized = pre_state.is_authorized; From 3393c5576843783a7e97c802f9f79268323e4220 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 3 Dec 2025 19:59:58 -0300 Subject: [PATCH 79/90] fix program deployment integration test --- integration_tests/src/data_changer.bin | Bin 371388 -> 371256 bytes .../guest/src/bin/data_changer.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/src/data_changer.bin b/integration_tests/src/data_changer.bin index c4fbec0f1c44d11a802a9f10e241da57d768ea88..6a36d52c37bd34dcb6e4b70f7fedc9f49ecff445 100644 GIT binary patch delta 106426 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 delta 106580 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 VVEtaJ9vxY-=Cav4rbchS=Km>I6x9F# 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 b0b1e19..2869d01 100644 --- a/nssa/test_program_methods/guest/src/bin/data_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/data_changer.rs @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.data.push(0); - write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); + write_nssa_outputs(vec![pre], vec![AccountPostState::new_claimed(account_post)]); } From 4ed86ce1822c328f6d19bf11dfb087934898c2dc Mon Sep 17 00:00:00 2001 From: fryorcraken Date: Thu, 4 Dec 2025 21:00:29 +1100 Subject: [PATCH 80/90] Add instructions for Fedora --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e35eacb..1a0d4a9 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,20 @@ Both public and private executions of the same program are enforced to use the s # Install dependencies Install build dependencies + - On Linux +Ubuntu / Debian ```sh apt install build-essential clang libssl-dev pkg-config ``` + +Fedora +```sh +sudo dnf install clang openssl-devel pkgconf llvm +``` + +> **Note for Fedora 41+ users:** GCC 14+ has stricter C++ standard library headers that cause build failures with the bundled RocksDB. You must set `CXXFLAGS="-include cstdint"` when running cargo commands. See the [Run tests](#run-tests) section for examples. + - On Mac ```sh xcode-select --install @@ -99,7 +109,10 @@ The NSSA repository includes both unit and integration test suites. ```bash # RISC0_DEV_MODE=1 is used to skip proof generation and reduce test runtime overhead -RISC0_DEV_MODE=1 cargo test --release +RISC0_DEV_MODE=1 cargo test --release + +# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build: +CXXFLAGS="-include cstdint" RISC0_DEV_MODE=1 cargo test --release ``` ### Integration tests @@ -109,6 +122,9 @@ export NSSA_WALLET_HOME_DIR=$(pwd)/integration_tests/configs/debug/wallet/ cd integration_tests # RISC0_DEV_MODE=1 skips proof generation; RUST_LOG=info enables runtime logs RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all + +# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build: +CXXFLAGS="-include cstdint" RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all ``` # Run the sequencer @@ -118,6 +134,9 @@ The sequencer can be run locally: ```bash cd sequencer_runner RUST_LOG=info cargo run --release configs/debug + +# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build: +CXXFLAGS="-include cstdint" RUST_LOG=info cargo run --release configs/debug ``` If everything went well you should see an output similar to this: @@ -142,6 +161,9 @@ This repository includes a CLI for interacting with the Nescience sequencer. To ```bash cargo install --path wallet --force + +# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build: +CXXFLAGS="-include cstdint" cargo install --path wallet --force ``` Before using the CLI, set the environment variable `NSSA_WALLET_HOME_DIR` to the directory containing the wallet configuration file. A sample configuration is available at `integration_tests/configs/debug/wallet/`. To use it, run: From 6b268112299a0b9234ef6b4b2c75af075dd26d9e Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 4 Dec 2025 14:55:45 +0300 Subject: [PATCH 81/90] feat: implement multiple blocks polling --- Cargo.toml | 1 + common/src/rpc_primitives/requests.rs | 13 +++++++ common/src/sequencer_client/mod.rs | 25 +++++++++++-- .../configs/debug/wallet/wallet_config.json | 4 +-- integration_tests/src/test_suite_map.rs | 10 +++--- sequencer_rpc/Cargo.toml | 1 + sequencer_rpc/src/process.rs | 25 ++++++++++++- wallet/Cargo.toml | 2 +- wallet/src/chain_storage.rs | 4 +-- wallet/src/cli/config.rs | 36 +++++++++---------- wallet/src/config.rs | 12 +++---- wallet/src/lib.rs | 12 ++++--- wallet/src/poller.rs | 34 +++++++++++------- 13 files changed, 124 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a4a2b89..a54b91a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ hmac-sha512 = "1.1.7" chrono = "0.4.41" borsh = "1.5.7" base58 = "0.2.0" +itertools = "0.14.0" rocksdb = { version = "0.21.0", default-features = false, features = [ "snappy", diff --git a/common/src/rpc_primitives/requests.rs b/common/src/rpc_primitives/requests.rs index 7149472..f87dc69 100644 --- a/common/src/rpc_primitives/requests.rs +++ b/common/src/rpc_primitives/requests.rs @@ -28,6 +28,13 @@ pub struct GetBlockDataRequest { pub block_id: u64, } +/// Get a range of blocks from `start_block_id` to `end_block_id` (inclusive) +#[derive(Serialize, Deserialize, Debug)] +pub struct GetBlockRangeDataRequest { + pub start_block_id: u64, + pub end_block_id: u64, +} + #[derive(Serialize, Deserialize, Debug)] pub struct GetGenesisIdRequest {} @@ -69,6 +76,7 @@ parse_request!(HelloRequest); parse_request!(RegisterAccountRequest); parse_request!(SendTxRequest); parse_request!(GetBlockDataRequest); +parse_request!(GetBlockRangeDataRequest); parse_request!(GetGenesisIdRequest); parse_request!(GetLastBlockRequest); parse_request!(GetInitialTestnetAccountsRequest); @@ -100,6 +108,11 @@ pub struct GetBlockDataResponse { pub block: Vec, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetBlockRangeDataResponse { + pub blocks: Vec>, +} + #[derive(Serialize, Deserialize, Debug)] pub struct GetGenesisIdResponse { pub genesis_id: u64, diff --git a/common/src/sequencer_client/mod.rs b/common/src/sequencer_client/mod.rs index a31806e..7a3956b 100644 --- a/common/src/sequencer_client/mod.rs +++ b/common/src/sequencer_client/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, ops::RangeInclusive}; use anyhow::Result; use json::{SendTxRequest, SendTxResponse, SequencerRpcRequest, SequencerRpcResponse}; @@ -14,7 +14,8 @@ use crate::{ error::{SequencerClientError, SequencerRpcError}, rpc_primitives::requests::{ GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse, - GetLastBlockRequest, GetLastBlockResponse, GetProgramIdsRequest, GetProgramIdsResponse, + GetBlockRangeDataRequest, GetBlockRangeDataResponse, GetLastBlockRequest, + GetLastBlockResponse, GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest, GetProofForCommitmentResponse, GetTransactionByHashRequest, GetTransactionByHashResponse, }, @@ -80,6 +81,26 @@ impl SequencerClient { Ok(resp_deser) } + pub async fn get_block_range( + &self, + range: RangeInclusive, + ) -> Result { + let block_req = GetBlockRangeDataRequest { + start_block_id: *range.start(), + end_block_id: *range.end(), + }; + + let req = serde_json::to_value(block_req)?; + + let resp = self + .call_method_with_payload("get_block_range", req) + .await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + /// Get last known `blokc_id` from sequencer pub async fn get_last_block(&self) -> Result { let block_req = GetLastBlockRequest {}; diff --git a/integration_tests/configs/debug/wallet/wallet_config.json b/integration_tests/configs/debug/wallet/wallet_config.json index 82f2864..ac4bae8 100644 --- a/integration_tests/configs/debug/wallet/wallet_config.json +++ b/integration_tests/configs/debug/wallet/wallet_config.json @@ -2,9 +2,9 @@ "override_rust_log": null, "sequencer_addr": "http://127.0.0.1:3040", "seq_poll_timeout_millis": 12000, - "seq_poll_max_blocks": 5, + "seq_tx_poll_max_blocks": 5, "seq_poll_max_retries": 5, - "seq_poll_retry_delay_millis": 500, + "seq_block_poll_max_amount": 100, "initial_accounts": [ { "Public": { diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 9903345..1c5f91f 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1646,23 +1646,23 @@ pub fn prepare_function_map() -> HashMap { info!("########## test_modify_config_fields ##########"); let wallet_config = fetch_config().await.unwrap(); - let old_seq_poll_retry_delay_millis = wallet_config.seq_poll_retry_delay_millis; + let old_seq_poll_timeout_millis = wallet_config.seq_poll_timeout_millis; // Change config field let command = Command::Config(ConfigSubcommand::Set { - key: "seq_poll_retry_delay_millis".to_string(), + key: "seq_poll_timeout_millis".to_string(), value: "1000".to_string(), }); wallet::cli::execute_subcommand(command).await.unwrap(); let wallet_config = fetch_config().await.unwrap(); - assert_eq!(wallet_config.seq_poll_retry_delay_millis, 1000); + assert_eq!(wallet_config.seq_poll_timeout_millis, 1000); // Return how it was at the beginning let command = Command::Config(ConfigSubcommand::Set { - key: "seq_poll_retry_delay_millis".to_string(), - value: old_seq_poll_retry_delay_millis.to_string(), + key: "seq_poll_timeout_millis".to_string(), + value: old_seq_poll_timeout_millis.to_string(), }); wallet::cli::execute_subcommand(command).await.unwrap(); diff --git a/sequencer_rpc/Cargo.toml b/sequencer_rpc/Cargo.toml index 242e8b2..395660f 100644 --- a/sequencer_rpc/Cargo.toml +++ b/sequencer_rpc/Cargo.toml @@ -14,6 +14,7 @@ base58.workspace = true hex = "0.4.3" tempfile.workspace = true base64.workspace = true +itertools.workspace = true actix-web.workspace = true tokio.workspace = true diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index 23d5edd..387abf2 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -13,7 +13,8 @@ use common::{ requests::{ GetAccountBalanceRequest, GetAccountBalanceResponse, GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse, - GetBlockDataRequest, GetBlockDataResponse, GetGenesisIdRequest, GetGenesisIdResponse, + GetBlockDataRequest, GetBlockDataResponse, GetBlockRangeDataRequest, + GetBlockRangeDataResponse, GetGenesisIdRequest, GetGenesisIdResponse, GetInitialTestnetAccountsRequest, GetLastBlockRequest, GetLastBlockResponse, GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest, GetProofForCommitmentResponse, GetTransactionByHashRequest, @@ -23,6 +24,7 @@ use common::{ }, transaction::{EncodedTransaction, NSSATransaction}, }; +use itertools::Itertools as _; use log::warn; use nssa::{self, program::Program}; use sequencer_core::{TransactionMalformationError, config::AccountInitialData}; @@ -33,6 +35,7 @@ use super::{JsonHandler, respond, types::err_rpc::RpcErr}; pub const HELLO: &str = "hello"; pub const SEND_TX: &str = "send_tx"; pub const GET_BLOCK: &str = "get_block"; +pub const GET_BLOCK_RANGE: &str = "get_block_range"; pub const GET_GENESIS: &str = "get_genesis"; pub const GET_LAST_BLOCK: &str = "get_last_block"; pub const GET_ACCOUNT_BALANCE: &str = "get_account_balance"; @@ -120,6 +123,25 @@ impl JsonHandler { respond(response) } + async fn process_get_block_range_data(&self, request: Request) -> Result { + let get_block_req = GetBlockRangeDataRequest::parse(Some(request.params))?; + + let blocks = { + let state = self.sequencer_state.lock().await; + (get_block_req.start_block_id..=get_block_req.end_block_id) + .map(|block_id| state.block_store().get_block_at_id(block_id)) + .map_ok(|block| { + borsh::to_vec(&HashableBlockData::from(block)) + .expect("derived BorshSerialize should never fail") + }) + .collect::, _>>()? + }; + + let response = GetBlockRangeDataResponse { blocks }; + + respond(response) + } + async fn process_get_genesis(&self, request: Request) -> Result { let _get_genesis_req = GetGenesisIdRequest::parse(Some(request.params))?; @@ -297,6 +319,7 @@ impl JsonHandler { HELLO => self.process_temp_hello(request).await, SEND_TX => self.process_send_tx(request).await, GET_BLOCK => self.process_get_block_data(request).await, + GET_BLOCK_RANGE => self.process_get_block_range_data(request).await, GET_GENESIS => self.process_get_genesis(request).await, GET_LAST_BLOCK => self.process_get_last_block(request).await, GET_INITIAL_TESTNET_ACCOUNTS => self.get_initial_testnet_accounts(request).await, diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 34fc84c..6f97c63 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -19,7 +19,7 @@ borsh.workspace = true base58.workspace = true hex = "0.4.3" rand.workspace = true -itertools = "0.14.0" +itertools.workspace = true sha2.workspace = true futures.workspace = true async-stream = "0.3.6" diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index 14e931a..0625fce 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -259,9 +259,9 @@ mod tests { override_rust_log: None, sequencer_addr: "http://127.0.0.1".to_string(), seq_poll_timeout_millis: 12000, - seq_poll_max_blocks: 5, + seq_tx_poll_max_blocks: 5, seq_poll_max_retries: 10, - seq_poll_retry_delay_millis: 500, + seq_block_poll_max_amount: 100, initial_accounts: create_initial_accounts(), } } diff --git a/wallet/src/cli/config.rs b/wallet/src/cli/config.rs index 68670af..df0413e 100644 --- a/wallet/src/cli/config.rs +++ b/wallet/src/cli/config.rs @@ -55,19 +55,19 @@ impl WalletSubcommand for ConfigSubcommand { wallet_core.storage.wallet_config.seq_poll_timeout_millis ); } - "seq_poll_max_blocks" => { - println!("{}", wallet_core.storage.wallet_config.seq_poll_max_blocks); + "seq_tx_poll_max_blocks" => { + println!( + "{}", + wallet_core.storage.wallet_config.seq_tx_poll_max_blocks + ); } "seq_poll_max_retries" => { println!("{}", wallet_core.storage.wallet_config.seq_poll_max_retries); } - "seq_poll_retry_delay_millis" => { + "seq_block_poll_max_amount" => { println!( "{}", - wallet_core - .storage - .wallet_config - .seq_poll_retry_delay_millis + wallet_core.storage.wallet_config.seq_block_poll_max_amount ); } "initial_accounts" => { @@ -89,17 +89,15 @@ impl WalletSubcommand for ConfigSubcommand { wallet_core.storage.wallet_config.seq_poll_timeout_millis = value.parse()?; } - "seq_poll_max_blocks" => { - wallet_core.storage.wallet_config.seq_poll_max_blocks = value.parse()?; + "seq_tx_poll_max_blocks" => { + wallet_core.storage.wallet_config.seq_tx_poll_max_blocks = value.parse()?; } "seq_poll_max_retries" => { wallet_core.storage.wallet_config.seq_poll_max_retries = value.parse()?; } - "seq_poll_retry_delay_millis" => { - wallet_core - .storage - .wallet_config - .seq_poll_retry_delay_millis = value.parse()?; + "seq_block_poll_max_amount" => { + wallet_core.storage.wallet_config.seq_block_poll_max_amount = + value.parse()?; } "initial_accounts" => { anyhow::bail!("Setting this field from wallet is not supported"); @@ -125,19 +123,19 @@ impl WalletSubcommand for ConfigSubcommand { "Sequencer client retry variable: how much time to wait between retries in milliseconds(can be zero)" ); } - "seq_poll_max_blocks" => { + "seq_tx_poll_max_blocks" => { println!( - "Sequencer client polling variable: max number of blocks to poll in parallel" + "Sequencer client polling variable: max number of blocks to poll to find a transaction" ); } "seq_poll_max_retries" => { println!( - "Sequencer client retry variable: MAX number of retries before failing(can be zero)" + "Sequencer client retry variable: max number of retries before failing(can be zero)" ); } - "seq_poll_retry_delay_millis" => { + "seq_block_poll_max_amount" => { println!( - "Sequencer client polling variable: how much time to wait in milliseconds between polling retries(can be zero)" + "Sequencer client polling variable: max number of blocks to request in one polling call" ); } "initial_accounts" => { diff --git a/wallet/src/config.rs b/wallet/src/config.rs index e11359e..ebcf283 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -135,12 +135,12 @@ pub struct WalletConfig { pub sequencer_addr: String, /// Sequencer polling duration for new blocks in milliseconds pub seq_poll_timeout_millis: u64, - /// Sequencer polling max number of blocks - pub seq_poll_max_blocks: usize, + /// Sequencer polling max number of blocks to find transaction + pub seq_tx_poll_max_blocks: usize, /// Sequencer polling max number error retries pub seq_poll_max_retries: u64, - /// Sequencer polling error retry delay in milliseconds - pub seq_poll_retry_delay_millis: u64, + /// Max amount of blocks to poll in one request + pub seq_block_poll_max_amount: u64, /// Initial accounts for wallet pub initial_accounts: Vec, } @@ -151,9 +151,9 @@ impl Default for WalletConfig { override_rust_log: None, sequencer_addr: "http://127.0.0.1:3040".to_string(), seq_poll_timeout_millis: 12000, - seq_poll_max_blocks: 5, + seq_tx_poll_max_blocks: 5, seq_poll_max_retries: 5, - seq_poll_retry_delay_millis: 500, + seq_block_poll_max_amount: 100, initial_accounts: { let init_acc_json = r#" [ diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 2886dcd..13812be 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -304,6 +304,8 @@ impl WalletCore { return Ok(()); } + let before_polling = std::time::Instant::now(); + let poller = self.poller.clone(); let mut blocks = std::pin::pin!(poller.poll_block_range(self.last_synced_block + 1..=block_id)); @@ -316,13 +318,13 @@ impl WalletCore { self.last_synced_block = block.block_id; self.store_persistent_data().await?; - - println!( - "Block at id {} with timestamp {} parsed", - block.block_id, block.timestamp, - ); } + println!( + "Synced to block {block_id} in {:?}", + before_polling.elapsed() + ); + Ok(()) } diff --git a/wallet/src/poller.rs b/wallet/src/poller.rs index 0e2192d..a96b1ae 100644 --- a/wallet/src/poller.rs +++ b/wallet/src/poller.rs @@ -9,21 +9,21 @@ use crate::config::WalletConfig; #[derive(Clone)] /// Helperstruct to poll transactions pub struct TxPoller { - pub polling_max_blocks_to_query: usize, - pub polling_max_error_attempts: u64, + polling_max_blocks_to_query: usize, + polling_max_error_attempts: u64, // TODO: This should be Duration - pub polling_error_delay_millis: u64, - pub polling_delay_millis: u64, - pub client: Arc, + polling_delay_millis: u64, + block_poll_max_amount: u64, + client: Arc, } impl TxPoller { pub fn new(config: WalletConfig, client: Arc) -> Self { Self { polling_delay_millis: config.seq_poll_timeout_millis, - polling_max_blocks_to_query: config.seq_poll_max_blocks, + polling_max_blocks_to_query: config.seq_tx_poll_max_blocks, polling_max_error_attempts: config.seq_poll_max_retries, - polling_error_delay_millis: config.seq_poll_retry_delay_millis, + block_poll_max_amount: config.seq_block_poll_max_amount, client: client.clone(), } } @@ -72,11 +72,21 @@ impl TxPoller { range: std::ops::RangeInclusive, ) -> impl futures::Stream> { async_stream::stream! { - for block_id in range { - let block = borsh::from_slice::( - &self.client.get_block(block_id).await?.block, - )?; - yield Ok(block); + let mut chunk_start = *range.start(); + + loop { + let chunk_end = std::cmp::min(chunk_start + self.block_poll_max_amount - 1, *range.end()); + + let blocks = self.client.get_block_range(chunk_start..=chunk_end).await?.blocks; + for block in blocks { + let block = borsh::from_slice::(&block)?; + yield Ok(block); + } + + chunk_start = chunk_end + 1; + if chunk_start > *range.end() { + break; + } } } } From 03e911ecd51f514baf07872bc3ac261bec610723 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Wed, 3 Dec 2025 18:33:40 +0300 Subject: [PATCH 82/90] feat: apply base64 encoding for large binary data transfer --- common/Cargo.toml | 1 + common/src/rpc_primitives/requests.rs | 61 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/common/Cargo.toml b/common/Cargo.toml index 999c731..920ad2a 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -15,6 +15,7 @@ log.workspace = true hex.workspace = true nssa-core = { path = "../nssa/core", features = ["host"] } borsh.workspace = true +base64.workspace = true [dependencies.nssa] path = "../nssa" diff --git a/common/src/rpc_primitives/requests.rs b/common/src/rpc_primitives/requests.rs index f87dc69..e0c6d31 100644 --- a/common/src/rpc_primitives/requests.rs +++ b/common/src/rpc_primitives/requests.rs @@ -20,6 +20,7 @@ pub struct RegisterAccountRequest { #[derive(Serialize, Deserialize, Debug)] pub struct SendTxRequest { + #[serde(with = "base64_deser")] pub transaction: Vec, } @@ -105,14 +106,74 @@ pub struct SendTxResponse { #[derive(Serialize, Deserialize, Debug)] pub struct GetBlockDataResponse { + #[serde(with = "base64_deser")] pub block: Vec, } #[derive(Serialize, Deserialize, Debug)] pub struct GetBlockRangeDataResponse { + #[serde(with = "base64_deser::vec")] pub blocks: Vec>, } +mod base64_deser { + use base64::{Engine as _, engine::general_purpose}; + use serde::{self, Deserialize, Deserializer, Serializer, ser::SerializeSeq as _}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result + where + S: Serializer, + { + let base64_string = general_purpose::STANDARD.encode(bytes); + serializer.serialize_str(&base64_string) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let base64_string: String = Deserialize::deserialize(deserializer)?; + general_purpose::STANDARD + .decode(&base64_string) + .map_err(serde::de::Error::custom) + } + + pub mod vec { + use super::*; + + pub fn serialize(bytes: &[Vec], serializer: S) -> Result + where + S: Serializer, + { + let base64_strings: Vec = bytes + .iter() + .map(|b| general_purpose::STANDARD.encode(b)) + .collect(); + let mut seq = serializer.serialize_seq(Some(base64_strings.len()))?; + for s in base64_strings { + seq.serialize_element(&s)?; + } + seq.end() + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + let base64_strings: Vec = Deserialize::deserialize(deserializer)?; + let bytes_vec: Result>, D::Error> = base64_strings + .into_iter() + .map(|s| { + general_purpose::STANDARD + .decode(&s) + .map_err(serde::de::Error::custom) + }) + .collect(); + bytes_vec + } + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct GetGenesisIdResponse { pub genesis_id: u64, From 1412ad4da4928cfe4e263cb78fd7c7c9fc6e365a Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 4 Dec 2025 03:51:09 +0300 Subject: [PATCH 83/90] refactor: remove redundant request and response types --- common/src/rpc_primitives/message.rs | 10 ++++ common/src/rpc_primitives/requests.rs | 23 ++++---- .../mod.rs => sequencer_client.rs} | 35 +++++++----- common/src/sequencer_client/json.rs | 53 ------------------- wallet/src/lib.rs | 3 +- .../native_token_transfer/deshielded.rs | 2 +- .../native_token_transfer/private.rs | 2 +- .../native_token_transfer/public.rs | 2 +- .../native_token_transfer/shielded.rs | 2 +- wallet/src/program_facades/pinata.rs | 2 +- wallet/src/program_facades/token.rs | 2 +- 11 files changed, 54 insertions(+), 82 deletions(-) rename common/src/{sequencer_client/mod.rs => sequencer_client.rs} (89%) delete mode 100644 common/src/sequencer_client/json.rs diff --git a/common/src/rpc_primitives/message.rs b/common/src/rpc_primitives/message.rs index 8207267..9886744 100644 --- a/common/src/rpc_primitives/message.rs +++ b/common/src/rpc_primitives/message.rs @@ -62,6 +62,16 @@ pub struct Request { } impl Request { + pub fn from_payload_version_2_0(method: String, payload: serde_json::Value) -> Self { + Self { + jsonrpc: Version, + method, + params: payload, + // ToDo: Correct checking of id + id: 1.into(), + } + } + /// Answer the request with a (positive) reply. /// /// The ID is taken from the request. diff --git a/common/src/rpc_primitives/requests.rs b/common/src/rpc_primitives/requests.rs index e0c6d31..7164193 100644 --- a/common/src/rpc_primitives/requests.rs +++ b/common/src/rpc_primitives/requests.rs @@ -141,16 +141,13 @@ mod base64_deser { pub mod vec { use super::*; - pub fn serialize(bytes: &[Vec], serializer: S) -> Result + pub fn serialize(bytes_vec: &[Vec], serializer: S) -> Result where S: Serializer, { - let base64_strings: Vec = bytes - .iter() - .map(|b| general_purpose::STANDARD.encode(b)) - .collect(); - let mut seq = serializer.serialize_seq(Some(base64_strings.len()))?; - for s in base64_strings { + let mut seq = serializer.serialize_seq(Some(bytes_vec.len()))?; + for bytes in bytes_vec { + let s = general_purpose::STANDARD.encode(bytes); seq.serialize_element(&s)?; } seq.end() @@ -161,15 +158,14 @@ mod base64_deser { D: Deserializer<'de>, { let base64_strings: Vec = Deserialize::deserialize(deserializer)?; - let bytes_vec: Result>, D::Error> = base64_strings + base64_strings .into_iter() .map(|s| { general_purpose::STANDARD .decode(&s) .map_err(serde::de::Error::custom) }) - .collect(); - bytes_vec + .collect() } } } @@ -213,3 +209,10 @@ pub struct GetProofForCommitmentResponse { pub struct GetProgramIdsResponse { pub program_ids: HashMap, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GetInitialTestnetAccountsResponse { + /// Hex encoded account id + pub account_id: String, + pub balance: u64, +} diff --git a/common/src/sequencer_client/mod.rs b/common/src/sequencer_client.rs similarity index 89% rename from common/src/sequencer_client/mod.rs rename to common/src/sequencer_client.rs index 7a3956b..d3c5f23 100644 --- a/common/src/sequencer_client/mod.rs +++ b/common/src/sequencer_client.rs @@ -1,9 +1,9 @@ use std::{collections::HashMap, ops::RangeInclusive}; use anyhow::Result; -use json::{SendTxRequest, SendTxResponse, SequencerRpcRequest, SequencerRpcResponse}; use nssa_core::program::ProgramId; use reqwest::Client; +use serde::Deserialize; use serde_json::Value; use super::rpc_primitives::requests::{ @@ -12,19 +12,20 @@ use super::rpc_primitives::requests::{ }; use crate::{ error::{SequencerClientError, SequencerRpcError}, - rpc_primitives::requests::{ - GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse, - GetBlockRangeDataRequest, GetBlockRangeDataResponse, GetLastBlockRequest, - GetLastBlockResponse, GetProgramIdsRequest, GetProgramIdsResponse, - GetProofForCommitmentRequest, GetProofForCommitmentResponse, GetTransactionByHashRequest, - GetTransactionByHashResponse, + rpc_primitives::{ + self, + requests::{ + GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest, + GetAccountsNoncesResponse, GetBlockRangeDataRequest, GetBlockRangeDataResponse, + GetInitialTestnetAccountsResponse, GetLastBlockRequest, GetLastBlockResponse, + GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest, + GetProofForCommitmentResponse, GetTransactionByHashRequest, + GetTransactionByHashResponse, SendTxRequest, SendTxResponse, + }, }, - sequencer_client::json::AccountInitialData, transaction::{EncodedTransaction, NSSATransaction}, }; -pub mod json; - #[derive(Clone)] pub struct SequencerClient { pub client: reqwest::Client, @@ -47,7 +48,8 @@ impl SequencerClient { method: &str, payload: Value, ) -> Result { - let request = SequencerRpcRequest::from_payload_version_2_0(method.to_string(), payload); + let request = + rpc_primitives::message::Request::from_payload_version_2_0(method.to_string(), payload); let call_builder = self.client.post(&self.sequencer_addr); @@ -55,6 +57,15 @@ impl SequencerClient { 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 { + pub jsonrpc: String, + pub result: serde_json::Value, + pub id: u64, + } + if let Ok(response) = serde_json::from_value::(response_vall.clone()) { Ok(response.result) @@ -244,7 +255,7 @@ impl SequencerClient { /// Get initial testnet accounts from sequencer pub async fn get_initial_testnet_accounts( &self, - ) -> Result, SequencerClientError> { + ) -> Result, SequencerClientError> { let acc_req = GetInitialTestnetAccountsRequest {}; let req = serde_json::to_value(acc_req).unwrap(); diff --git a/common/src/sequencer_client/json.rs b/common/src/sequencer_client/json.rs deleted file mode 100644 index d47aea4..0000000 --- a/common/src/sequencer_client/json.rs +++ /dev/null @@ -1,53 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// Requests - -#[derive(Serialize, Deserialize, Debug)] -pub struct SendTxRequest { - pub transaction: Vec, -} - -// Responses - -#[derive(Serialize, Deserialize, Debug)] -pub struct SendTxResponse { - pub status: String, - pub tx_hash: String, -} - -// General - -#[derive(Debug, Clone, Serialize)] -pub struct SequencerRpcRequest { - jsonrpc: String, - pub method: String, - pub params: serde_json::Value, - pub id: u64, -} - -impl SequencerRpcRequest { - pub fn from_payload_version_2_0(method: String, payload: serde_json::Value) -> Self { - Self { - jsonrpc: "2.0".to_string(), - method, - params: payload, - // ToDo: Correct checking of id - id: 1, - } - } -} - -#[derive(Debug, Clone, Deserialize)] -pub struct SequencerRpcResponse { - pub jsonrpc: String, - pub result: serde_json::Value, - pub id: u64, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -/// Helperstruct for account serialization -pub struct AccountInitialData { - /// Hex encoded account id - pub account_id: String, - pub balance: u64, -} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 13812be..91a0e4b 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -5,7 +5,8 @@ use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; use chain_storage::WalletChainStore; use common::{ error::ExecutionFailureKind, - sequencer_client::{SequencerClient, json::SendTxResponse}, + rpc_primitives::requests::SendTxResponse, + sequencer_client::SequencerClient, transaction::{EncodedTransaction, NSSATransaction}, }; use config::WalletConfig; diff --git a/wallet/src/program_facades/native_token_transfer/deshielded.rs b/wallet/src/program_facades/native_token_transfer/deshielded.rs index a25be2c..35a13ba 100644 --- a/wallet/src/program_facades/native_token_transfer/deshielded.rs +++ b/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -1,4 +1,4 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::AccountId; use super::{NativeTokenTransfer, auth_transfer_preparation}; diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs index fcf6eee..320027b 100644 --- a/wallet/src/program_facades/native_token_transfer/private.rs +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -1,6 +1,6 @@ use std::vec; -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::{AccountId, program::Program}; use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; diff --git a/wallet/src/program_facades/native_token_transfer/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index 2edab15..7981c19 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -1,4 +1,4 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::{ AccountId, PublicTransaction, program::Program, diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index c049b13..0802d6e 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -1,4 +1,4 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::AccountId; use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs index 46bc7a1..41e7510 100644 --- a/wallet/src/program_facades/pinata.rs +++ b/wallet/src/program_facades/pinata.rs @@ -1,4 +1,4 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::AccountId; use nssa_core::SharedSecretKey; diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 298c4f4..7c97155 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -1,4 +1,4 @@ -use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::{AccountId, program::Program}; use nssa_core::{ NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey, From fe83a20c4da88ecc9f5b4f81d2814d6f9c84ed98 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Thu, 4 Dec 2025 14:34:11 +0200 Subject: [PATCH 84/90] fix: suggestion 1 --- wallet/src/cli/account.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index f6bc90a..da1734e 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -179,15 +179,10 @@ impl From for TokedDefinitionAccountView { Self { account_type: "Token definition".to_string(), name: { - let mut name_vec_trim = vec![]; - for ch in value.name { - // Assuming, that name does not have UTF-8 NULL and all zeroes are padding. - if ch == 0 { - break; - } - name_vec_trim.push(ch); - } - String::from_utf8(name_vec_trim).unwrap_or(hex::encode(value.name)) + // Assuming, that name does not have UTF-8 NULL and all zeroes are padding. + let name_trimmed: Vec<_> = + value.name.into_iter().take_while(|ch| *ch != 0).collect(); + String::from_utf8(name_trimmed).unwrap_or(hex::encode(value.name)) }, total_supply: value.total_supply, } From 686eb787c9d0b2105f6d24bca3dd553856d39179 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 4 Dec 2025 10:02:29 -0300 Subject: [PATCH 85/90] fix test names --- nssa/core/src/program.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 744c1dc..5912724 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -164,7 +164,7 @@ mod tests { use super::*; #[test] - fn test_post_state_new_without_claim_constructor() { + fn test_post_state_new_with_claim_constructor() { let account = Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 1337, @@ -179,7 +179,7 @@ mod tests { } #[test] - fn test_post_state_new_with_claim_constructor() { + fn test_post_state_new_without_claim_constructor() { let account = Account { program_owner: [1, 2, 3, 4, 5, 6, 7, 8], balance: 1337, From 068bfa0ec59b1ba27cd5097eb438a24d1f846c35 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 4 Dec 2025 10:10:01 -0300 Subject: [PATCH 86/90] add docstrings. Remove unused method --- nssa/core/src/program.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 5912724..a5c92d7 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -20,6 +20,10 @@ pub struct ChainedCall { pub pre_states: Vec, } +/// Represents the final state of an `Account` after a program execution. +/// A post state may optionally request that the executing program +/// becomes the owner of the account (a “claim”). This is used to signal +/// that the program intends to take ownership of the account. #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct AccountPostState { @@ -28,6 +32,8 @@ pub struct AccountPostState { } impl AccountPostState { + /// Creates a post state without a claim request. + /// The executing program is not requesting ownership of the account. pub fn new(account: Account) -> Self { Self { account, @@ -35,6 +41,9 @@ impl AccountPostState { } } + /// Creates a post state that requests ownership of the account. + /// This indicates that the executing program intends to claim the + /// account as its own and is allowed to mutate it. pub fn new_claimed(account: Account) -> Self { Self { account, @@ -42,18 +51,13 @@ impl AccountPostState { } } + /// Returns `true` if this post state requests that the account + /// be claimed (owned) by the executing program. pub fn requires_claim(&self) -> bool { self.claim } } -impl AccountPostState { - pub fn with_claim_request(mut self) -> Self { - self.claim = true; - self - } -} - #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ProgramOutput { From cf9c567e29eeda0358c3dcf9532f14cd105fd1a2 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 4 Dec 2025 16:26:40 -0300 Subject: [PATCH 87/90] remove pub attribute --- nssa/core/src/program.rs | 27 ++++++++++++++++++- .../guest/src/bin/authenticated_transfer.rs | 2 +- .../src/bin/privacy_preserving_circuit.rs | 4 +-- nssa/program_methods/guest/src/bin/token.rs | 12 ++++----- nssa/src/program.rs | 4 +-- nssa/src/public_transaction/transaction.rs | 6 ++--- 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index a5c92d7..7abcafb 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -27,7 +27,7 @@ pub struct ChainedCall { #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct AccountPostState { - pub account: Account, + account: Account, claim: bool, } @@ -56,6 +56,16 @@ impl AccountPostState { pub fn requires_claim(&self) -> bool { self.claim } + + /// Returns the underlying account + pub fn account(&self) -> &Account { + &self.account + } + + /// Returns the underlying account + pub fn account_mut(&mut self) -> &mut Account { + &mut self.account + } } #[derive(Serialize, Deserialize, Clone)] @@ -196,4 +206,19 @@ mod tests { assert_eq!(account, account_post_state.account); assert!(!account_post_state.requires_claim()); } + + #[test] + fn test_post_state_account_getter() { + let mut account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef], + nonce: 10, + }; + + let mut account_post_state = AccountPostState::new(account.clone()); + + assert_eq!(account_post_state.account(), &account); + assert_eq!(account_post_state.account_mut(), &mut account); + } } diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index c9fc10b..e72e027 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -11,7 +11,7 @@ fn initialize_account(pre_state: AccountWithMetadata) { let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values - if account_to_claim.account != Account::default() { + if account_to_claim.account() != &Account::default() { return; } 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 e822f88..7813fa5 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -70,7 +70,7 @@ fn main() { // Public account public_pre_states.push(pre_states[i].clone()); - let mut post = post_states[i].account.clone(); + let mut post = post_states[i].account().clone(); if pre_states[i].is_authorized { post.nonce += 1; } @@ -126,7 +126,7 @@ fn main() { } // Update post-state with new nonce - let mut post_with_updated_values = post_states[i].account.clone(); + let mut post_with_updated_values = post_states[i].account().clone(); post_with_updated_values.nonce = *new_nonce; if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID { diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index ce4558a..9d5f31c 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -402,14 +402,14 @@ 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, + definition_account.account().data, vec![ 0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); assert_eq!( - holding_account.account.data, + holding_account.account().data, vec![ 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -634,14 +634,14 @@ 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, + sender_post.account().data, vec![ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ); assert_eq!( - recipient_post.account.data, + recipient_post.account().data, vec![ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 @@ -672,9 +672,9 @@ 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!(definition.account().data, pre_states[0].account.data); assert_eq!( - holding.account.data, + holding.account().data, vec![ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 91328b5..5acbe3e 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -248,8 +248,8 @@ mod tests { let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap(); - assert_eq!(sender_post.account, expected_sender_post); - assert_eq!(recipient_post.account, expected_recipient_post); + assert_eq!(sender_post.account(), &expected_sender_post); + assert_eq!(recipient_post.account(), &expected_recipient_post); } #[test] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 5ab0918..7e4343d 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -159,8 +159,8 @@ impl PublicTransaction { } // The invoked program can only claim accounts with default program id. - if post.account.program_owner == DEFAULT_PROGRAM_ID { - post.account.program_owner = chained_call.program_id; + if post.account().program_owner == DEFAULT_PROGRAM_ID { + post.account_mut().program_owner = chained_call.program_id; } else { return Err(NssaError::InvalidProgramBehavior); } @@ -172,7 +172,7 @@ impl PublicTransaction { .iter() .zip(program_output.post_states.iter()) { - state_diff.insert(pre.account_id, post.account.clone()); + state_diff.insert(pre.account_id, post.account().clone()); } for new_call in program_output.chained_calls.into_iter().rev() { From b5589d53bb873146a25c9cd75ea146c4e6fc57c6 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 4 Dec 2025 16:29:00 -0300 Subject: [PATCH 88/90] use filter --- nssa/src/public_transaction/transaction.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 7e4343d..e1818a8 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -153,11 +153,11 @@ impl PublicTransaction { return Err(NssaError::InvalidProgramBehavior); } - for post in program_output.post_states.iter_mut() { - if !post.requires_claim() { - continue; - } - + for post in program_output + .post_states + .iter_mut() + .filter(|post| post.requires_claim()) + { // The invoked program can only claim accounts with default program id. if post.account().program_owner == DEFAULT_PROGRAM_ID { post.account_mut().program_owner = chained_call.program_id; From a84b18f22ca5f29e25b36e39a7d5371472e5a42f Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 4 Dec 2025 16:46:43 -0300 Subject: [PATCH 89/90] remove unnecessary type annotation --- nssa/program_methods/guest/src/bin/authenticated_transfer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index e72e027..50afa50 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -37,7 +37,7 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance } // Create accounts post states, with updated balances - let sender_post: AccountPostState = { + let sender_post = { // Modify sender's balance let mut sender_post_account = sender.account.clone(); sender_post_account.balance -= balance_to_move; From 925ae8d0c165f0c8b5aaad528ecbb62deb25ea1f Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 4 Dec 2025 21:34:47 -0300 Subject: [PATCH 90/90] fmt --- nssa/core/src/program.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 03a2b7e..37a87da 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,10 +1,9 @@ use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; -use crate::account::{Account, AccountWithMetadata}; - #[cfg(feature = "host")] use crate::account::AccountId; +use crate::account::{Account, AccountWithMetadata}; pub type ProgramId = [u32; 8]; pub type InstructionData = Vec;