diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 1cf71ad..55be519 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1234,7 +1234,6 @@ pub fn prepare_function_map() -> HashMap { // }; // let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; - // println!("Waiting for next blocks to check if continoius run fetch account"); // tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -1397,6 +1396,7 @@ pub fn prepare_function_map() -> HashMap { pub async fn test_pinata() { info!("########## test_pinata ##########"); let pinata_account_id = PINATA_BASE58; + let pinata_prize = 150; let solution = 989106; let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 66a957c..054f993 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -17,7 +17,7 @@ pub struct ProgramInput { pub struct ChainedCall { pub program_id: ProgramId, pub instruction_data: InstructionData, - pub account_indices: Vec, + pub pre_states: Vec, } #[derive(Serialize, Deserialize, Clone)] @@ -25,7 +25,7 @@ pub struct ChainedCall { pub struct ProgramOutput { pub pre_states: Vec, pub post_states: Vec, - pub chained_call: Option, + pub chained_calls: Vec, } pub fn read_nssa_inputs() -> ProgramInput { @@ -42,7 +42,7 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec let output = ProgramOutput { pre_states, post_states, - chained_call: None, + chained_calls: Vec::new(), }; env::commit(&output); } @@ -50,12 +50,12 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec pub fn write_nssa_outputs_with_chained_call( pre_states: Vec, post_states: Vec, - chained_call: Option, + chained_calls: Vec, ) { let output = ProgramOutput { pre_states, post_states, - chained_call, + chained_calls, }; env::commit(&output); } diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index d8ed15d..6696245 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -27,11 +27,11 @@ fn main() { let ProgramOutput { pre_states, post_states, - chained_call, + chained_calls, } = program_output; // TODO: implement chained calls for privacy preserving transactions - if chained_call.is_some() { + if !chained_calls.is_empty() { panic!("Privacy preserving transactions do not support yet chained calls.") } diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 8ed9657..45d5310 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -54,4 +54,7 @@ pub enum NssaError { #[error("Program already exists")] ProgramAlreadyExists, + + #[error("Chain of calls is too long")] + MaxChainedCallsDepthExceeded, } diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 8b213a3..28f33fb 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -1,9 +1,9 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap, HashSet, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, - program::{DEFAULT_PROGRAM_ID, validate_execution}, + program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; @@ -11,6 +11,7 @@ use crate::{ V02State, error::NssaError, public_transaction::{Message, WitnessSet}, + state::MAX_NUMBER_CHAINED_CALLS, }; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] @@ -18,7 +19,6 @@ pub struct PublicTransaction { message: Message, witness_set: WitnessSet, } -const MAX_NUMBER_CHAINED_CALLS: usize = 10; impl PublicTransaction { pub fn new(message: Message, witness_set: WitnessSet) -> Self { @@ -89,7 +89,7 @@ impl PublicTransaction { } // Build pre_states for execution - let mut input_pre_states: Vec<_> = message + let input_pre_states: Vec<_> = message .account_ids .iter() .map(|account_id| { @@ -103,22 +103,44 @@ impl PublicTransaction { let mut state_diff: HashMap = HashMap::new(); - let mut program_id = message.program_id; - let mut instruction_data = message.instruction_data.clone(); + let initial_call = ChainedCall { + program_id: message.program_id, + instruction_data: message.instruction_data.clone(), + pre_states: input_pre_states, + }; + + let mut chained_calls = VecDeque::from_iter([initial_call]); + let mut chain_calls_counter = 0; + + while let Some(chained_call) = chained_calls.pop_front() { + if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS { + return Err(NssaError::MaxChainedCallsDepthExceeded); + } - for _i in 0..MAX_NUMBER_CHAINED_CALLS { // Check the `program_id` corresponds to a deployed program - let Some(program) = state.programs().get(&program_id) else { + let Some(program) = state.programs().get(&chained_call.program_id) else { return Err(NssaError::InvalidInput("Unknown program".into())); }; - let mut program_output = program.execute(&input_pre_states, &instruction_data)?; + let mut program_output = + program.execute(&chained_call.pre_states, &chained_call.instruction_data)?; - // This check is equivalent to checking that the program output pre_states coinicide - // with the values in the public state or with any modifications to those values - // during the chain of calls. - if input_pre_states != program_output.pre_states { - return Err(NssaError::InvalidProgramBehavior); + for pre in &program_output.pre_states { + let account_id = pre.account_id; + // Check that the program output pre_states coinicide with the values in the public + // state or with any modifications to those values during the chain of calls. + let expected_pre = state_diff + .get(&account_id) + .cloned() + .unwrap_or_else(|| state.get_account_by_id(&account_id)); + if pre.account != expected_pre { + return Err(NssaError::InvalidProgramBehavior); + } + + // Check that authorization flags are consistent with the provided ones + if pre.is_authorized && !signer_account_ids.contains(&account_id) { + return Err(NssaError::InvalidProgramBehavior); + } } // Verify execution corresponds to a well-behaved program. @@ -126,7 +148,7 @@ impl PublicTransaction { if !validate_execution( &program_output.pre_states, &program_output.post_states, - program_id, + chained_call.program_id, ) { return Err(NssaError::InvalidProgramBehavior); } @@ -134,7 +156,7 @@ impl PublicTransaction { // The invoked program claims the accounts with default program id. for post in program_output.post_states.iter_mut() { if post.program_owner == DEFAULT_PROGRAM_ID { - post.program_owner = program_id; + post.program_owner = chained_call.program_id; } } @@ -147,37 +169,11 @@ impl PublicTransaction { state_diff.insert(pre.account_id, post.clone()); } - if let Some(next_chained_call) = program_output.chained_call { - program_id = next_chained_call.program_id; - instruction_data = next_chained_call.instruction_data; + for new_call in program_output.chained_calls.into_iter().rev() { + chained_calls.push_front(new_call); + } - // Build post states with metadata for next call - let mut post_states_with_metadata = Vec::new(); - for (pre, post) in program_output - .pre_states - .iter() - .zip(program_output.post_states) - { - let mut post_with_metadata = pre.clone(); - post_with_metadata.account = post.clone(); - post_states_with_metadata.push(post_with_metadata); - } - - input_pre_states = next_chained_call - .account_indices - .iter() - .map(|&i| { - post_states_with_metadata - .get(i) - .ok_or_else(|| { - NssaError::InvalidInput("Invalid account indices".into()) - }) - .cloned() - }) - .collect::, NssaError>>()?; - } else { - break; - }; + chain_calls_counter += 1; } Ok(state_diff) diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 26e0e18..cef7791 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -13,6 +13,8 @@ use crate::{ public_transaction::PublicTransaction, }; +pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; + pub(crate) struct CommitmentSet { merkle_tree: MerkleTree, commitments: HashMap, @@ -261,6 +263,7 @@ pub mod tests { program::Program, public_transaction, signature::PrivateKey, + state::MAX_NUMBER_CHAINED_CALLS, }; fn transfer_transaction( @@ -2084,30 +2087,30 @@ pub mod tests { } #[test] - fn test_chained_call() { + fn test_chained_call_succeeds() { let program = Program::chain_caller(); let key = PrivateKey::try_new([1; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let from = AccountId::from(&PublicKey::new_from_private_key(&key)); + let to = AccountId::new([2; 32]); let initial_balance = 100; - let initial_data = [(account_id, initial_balance)]; + let initial_data = [(from, initial_balance), (to, 0)]; 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; - let instruction: (u128, ProgramId) = - (amount, Program::authenticated_transfer_program().id()); + let amount: u128 = 0; + let instruction: (u128, ProgramId, u32) = + (amount, Program::authenticated_transfer_program().id(), 2); let expected_to_post = Account { - program_owner: Program::chain_caller().id(), - balance: amount, + program_owner: Program::authenticated_transfer_program().id(), + balance: amount * 2, // The `chain_caller` chains the program twice ..Account::default() }; let message = public_transaction::Message::try_new( program.id(), - vec![to, from], // The chain_caller program permutes the account order in the call + vec![to, from], // The chain_caller program permutes the account order in the chain + // call vec![0], instruction, ) @@ -2119,7 +2122,44 @@ pub mod tests { 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); + // The `chain_caller` program calls the program twice + assert_eq!(from_post.balance, initial_balance - 2 * amount); assert_eq!(to_post, expected_to_post); } + + #[test] + fn test_execution_fails_if_chained_calls_exceeds_depth() { + let program = Program::chain_caller(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&key)); + let to = AccountId::new([2; 32]); + let initial_balance = 100; + 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(), + MAX_NUMBER_CHAINED_CALLS as u32 + 1, + ); + + let message = public_transaction::Message::try_new( + program.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![0], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx); + assert!(matches!( + result, + Err(NssaError::MaxChainedCallsDepthExceeded) + )); + } } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index dfd77b1..028f8a0 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -3,14 +3,14 @@ use nssa_core::program::{ }; use risc0_zkvm::serde::to_vec; -type Instruction = (u128, ProgramId); +type Instruction = (u128, ProgramId, u32); -/// A program that calls another program. +/// A program that calls another program `num_chain_calls` times. /// It permutes the order of the input accounts on the subsequent call fn main() { let ProgramInput { pre_states, - instruction: (balance, program_id), + instruction: (balance, program_id, num_chain_calls), } = read_nssa_inputs::(); let [sender_pre, receiver_pre] = match pre_states.try_into() { @@ -20,10 +20,19 @@ fn main() { let instruction_data = to_vec(&balance).unwrap(); - let chained_call = Some(ChainedCall { + 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 + }; + num_chain_calls as usize - 1 + ]; + + chained_call.push(ChainedCall { program_id, instruction_data, - account_indices: vec![1, 0], // <- Account order permutation here + pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here }); write_nssa_outputs_with_chained_call( diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index b04d67e..74eb5bc 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -19,6 +19,7 @@ borsh.workspace = true base58.workspace = true hex = "0.4.3" rand.workspace = true +itertools = "0.14.0" [dependencies.key_protocol] path = "../key_protocol" diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index b06a744..ee71f82 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -2,6 +2,7 @@ use anyhow::Result; use base58::ToBase58; use clap::Subcommand; use key_protocol::key_management::key_tree::chain_index::ChainIndex; +use itertools::Itertools as _; use nssa::{Account, AccountId, program::Program}; use serde::Serialize; @@ -84,6 +85,9 @@ pub enum AccountSubcommand { New(NewSubcommand), /// Sync private accounts SyncPrivate {}, + /// List all accounts owned by the wallet + #[command(visible_alias = "ls")] + List {}, } /// Represents generic register CLI subcommand @@ -304,6 +308,37 @@ impl WalletSubcommand for AccountSubcommand { Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block)) } + AccountSubcommand::List {} => { + let user_data = &wallet_core.storage.user_data; + let accounts = user_data + .default_pub_account_signing_keys + .keys() + .map(|id| format!("Preconfigured Public/{id}")) + .chain( + user_data + .default_user_private_accounts + .keys() + .map(|id| format!("Preconfigured Private/{id}")), + ) + .chain( + user_data + .public_key_tree + .account_id_map + .iter() + .map(|(id, chain_index)| format!("{chain_index} Public/{id}")), + ) + .chain( + user_data + .private_key_tree + .account_id_map + .iter() + .map(|(id, chain_index)| format!("{chain_index} Private/{id}")), + ) + .format(",\n"); + + println!("{accounts}"); + Ok(SubcommandReturnValue::Empty) + } } } }