From d82f06593da312dba61757f17357c8855b9f3b87 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 27 Nov 2025 12:08:27 -0300 Subject: [PATCH 01/35] 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 02/35] 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 03/35] 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 1989fd25a178eca653992ef3c47e1541eb490378 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 28 Nov 2025 11:10:00 -0300 Subject: [PATCH 04/35] 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 05/35] 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 bfbd50e8cbe2ebf17efda6841dca98553cb75cfd Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 28 Nov 2025 17:09:38 -0300 Subject: [PATCH 06/35] 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 07/35] 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 08/35] 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 09/35] 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 10/35] 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 3d529d19fa82e5f37c88fd4cdc0f11c62b8875b1 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 2 Dec 2025 10:48:21 -0300 Subject: [PATCH 11/35] 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 12/35] 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 13/35] 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 14/35] 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 15/35] 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 16/35] 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 17/35] 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 18/35] 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 19/35] 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 20/35] 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 21/35] 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 22/35] 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 23/35] 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 24/35] 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 25/35] 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 26/35] 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 27/35] 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 28/35] 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 29/35] 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 30/35] 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 31/35] 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 32/35] 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 33/35] 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 34/35] 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 35/35] 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;