diff --git a/Cargo.lock b/Cargo.lock index 40434dcc..4733c31d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2668,6 +2668,7 @@ dependencies = [ "secp256k1", "serde", "sha2", + "test-case", "test_program_methods", "thiserror", ] @@ -4315,6 +4316,39 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "test-case-core", +] + [[package]] name = "test_program_methods" version = "0.1.0" diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 354e6556..1a2fabca 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 1b72c464..73d5fec1 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 91d2b934..278e88f4 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 8da8b894..4a4c8bb6 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 404f32b1..d8f5915e 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 22693413..5f6d3781 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 6bbc5071..96e339c2 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 56b017e7..731d2dc7 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin new file mode 100644 index 00000000..692d152b Binary files /dev/null and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 0f42aaed..da0a9bef 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 99089d33..86fde894 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index ffb622f5..1ae1cd98 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin new file mode 100644 index 00000000..8f80ab58 Binary files /dev/null and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 08a57ea4..50199403 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 891acd58..4994ae3f 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 17793836..796de2d3 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 9dcb36a9..017de8b3 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index fba84578..23195ef2 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 6daa6a11..db1f87fa 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 4fa42b26..17c95475 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/examples/program_deployment/src/bin/run_hello_world_private.rs b/examples/program_deployment/src/bin/run_hello_world_private.rs index cb66d42f..27ac2079 100644 --- a/examples/program_deployment/src/bin/run_hello_world_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_private.rs @@ -50,7 +50,7 @@ async fn main() { wallet_core .send_privacy_preserving_tx( accounts, - &Program::serialize_instruction(greeting).unwrap(), + Program::serialize_instruction(greeting).unwrap(), &program.into(), ) .await diff --git a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs index bd67cfe6..9b3619cb 100644 --- a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs @@ -58,7 +58,7 @@ async fn main() { wallet_core .send_privacy_preserving_tx( accounts, - &Program::serialize_instruction(instruction).unwrap(), + Program::serialize_instruction(instruction).unwrap(), &program_with_dependencies, ) .await diff --git a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs index d879ab6f..fc116241 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs @@ -101,7 +101,7 @@ async fn main() { wallet_core .send_privacy_preserving_tx( accounts, - &Program::serialize_instruction(instruction).unwrap(), + Program::serialize_instruction(instruction).unwrap(), &program.into(), ) .await @@ -142,7 +142,7 @@ async fn main() { wallet_core .send_privacy_preserving_tx( accounts, - &Program::serialize_instruction(instruction).unwrap(), + Program::serialize_instruction(instruction).unwrap(), &program.into(), ) .await diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index c627cea2..a3f2b4b7 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -11,11 +11,13 @@ use tokio::test; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, - programs::pinata::PinataProgramAgnosticSubcommand, + programs::{ + native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, + }, }; #[test] -async fn claim_pinata_to_public_account() -> Result<()> { +async fn claim_pinata_to_existing_public_account() -> Result<()> { let mut ctx = TestContext::new().await?; let pinata_prize = 150; @@ -120,8 +122,26 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { anyhow::bail!("Expected RegisterAccount return value"); }; + let winner_account_id_formatted = format_private_account_id(&winner_account_id.to_string()); + + // Initialize account under auth transfer program + let command = Command::AuthTransfer(AuthTransferSubcommand::Init { + account_id: winner_account_id_formatted.clone(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let new_commitment = ctx + .wallet() + .get_private_account_commitment(&winner_account_id) + .context("Failed to get private account commitment")?; + assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await); + + // Claim pinata to the new private account let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: format_private_account_id(&winner_account_id.to_string()), + to: winner_account_id_formatted, }); let pinata_balance_pre = ctx diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index b62a3ce7..10cf53e7 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -234,16 +234,16 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { ]], ); let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &Program::serialize_instruction(balance_to_move).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![sender_pre, recipient_pre], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ (sender_npk.clone(), sender_ss), (recipient_npk.clone(), recipient_ss), ], - &[sender_nsk], - &[Some(proof)], + vec![sender_nsk], + vec![Some(proof)], &program.into(), ) .unwrap(); diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index f1c7709b..a508cc08 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -24,8 +24,9 @@ risc0-binfmt = "3.0.2" [dev-dependencies] test_program_methods.workspace = true -hex-literal = "1.0.0" env_logger.workspace = true +hex-literal = "1.0.0" +test-case = "3.3.1" [features] default = [] diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 51467afe..55ab0ded 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -15,9 +15,8 @@ pub type Nonce = u128; /// Account to be used both in public and private contexts #[derive( - Clone, Default, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, + Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, )] -#[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct Account { pub program_owner: ProgramId, pub balance: u128, @@ -25,8 +24,7 @@ pub struct Account { pub nonce: Nonce, } -#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] -#[cfg_attr(any(feature = "host", test), derive(Debug))] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct AccountWithMetadata { pub account: Account, pub is_authorized: bool, @@ -45,6 +43,7 @@ impl AccountWithMetadata { } #[derive( + Debug, Default, Copy, Clone, @@ -56,7 +55,7 @@ impl AccountWithMetadata { BorshSerialize, BorshDeserialize, )] -#[cfg_attr(any(feature = "host", test), derive(Debug, PartialOrd, Ord))] +#[cfg_attr(any(feature = "host", test), derive(PartialOrd, Ord))] pub struct AccountId { value: [u8; 32], } diff --git a/nssa/core/src/account/data.rs b/nssa/core/src/account/data.rs index 974cb06c..396bbe6e 100644 --- a/nssa/core/src/account/data.rs +++ b/nssa/core/src/account/data.rs @@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize}; pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB -#[derive(Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)] -#[cfg_attr(any(feature = "host", test), derive(Debug))] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)] pub struct Data(Vec); impl Data { diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index ec30700b..8d9d59fa 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use crate::{Commitment, account::AccountId}; -#[derive(Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, Hash))] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(any(feature = "host", test), derive(Clone, Hash))] pub struct NullifierPublicKey(pub [u8; 32]); impl From<&NullifierPublicKey> for AccountId { diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 357a4a58..32b3e2c0 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -30,6 +30,20 @@ impl PdaSeed { } } +pub fn compute_authorized_pdas( + caller_program_id: Option, + pda_seeds: &[PdaSeed], +) -> HashSet { + caller_program_id + .map(|caller_program_id| { + pda_seeds + .iter() + .map(|pda_seed| AccountId::from((&caller_program_id, pda_seed))) + .collect() + }) + .unwrap_or_default() +} + impl From<(&ProgramId, &PdaSeed)> for AccountId { fn from(value: (&ProgramId, &PdaSeed)) -> Self { use risc0_zkvm::sha::{Impl, Sha256}; @@ -93,6 +107,13 @@ impl AccountPostState { } } + /// Creates a post state that requests ownership of the account + /// if the account's program owner is the default program ID. + pub fn new_claimed_if_default(account: Account) -> Self { + let claim = account.program_owner == DEFAULT_PROGRAM_ID; + Self { account, claim } + } + /// Returns `true` if this post state requests that the account /// be claimed (owned) by the executing program. pub fn requires_claim(&self) -> bool { @@ -108,6 +129,11 @@ impl AccountPostState { pub fn account_mut(&mut self) -> &mut Account { &mut self.account } + + /// Consumes the post state and returns the underlying account + pub fn into_account(self) -> Account { + self.account + } } #[derive(Serialize, Deserialize, Clone)] diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index f29eb1c5..1b490de8 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, account::AccountWithMetadata, - program::{InstructionData, ProgramId, ProgramOutput}, + program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; @@ -43,27 +43,44 @@ impl From for ProgramWithDependencies { } /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution -/// circuit +/// circuit. #[expect(clippy::too_many_arguments, reason = "TODO: fix later")] pub fn execute_and_prove( - pre_states: &[AccountWithMetadata], - instruction_data: &InstructionData, - visibility_mask: &[u8], - private_account_nonces: &[u128], - private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], - private_account_nsks: &[NullifierSecretKey], - private_account_membership_proofs: &[Option], + pre_states: Vec, + instruction_data: InstructionData, + visibility_mask: Vec, + private_account_nonces: Vec, + private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, + private_account_nsks: Vec, + private_account_membership_proofs: Vec>, program_with_dependencies: &ProgramWithDependencies, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { - let mut program = &program_with_dependencies.program; - let dependencies = &program_with_dependencies.dependencies; - let mut instruction_data = instruction_data.clone(); - let mut pre_states = pre_states.to_vec(); + let ProgramWithDependencies { + program, + dependencies, + } = program_with_dependencies; let mut env_builder = ExecutorEnv::builder(); let mut program_outputs = Vec::new(); - for _i in 0..MAX_NUMBER_CHAINED_CALLS { - let inner_receipt = execute_and_prove_program(program, &pre_states, &instruction_data)?; + let initial_call = ChainedCall { + program_id: program.id(), + instruction_data: instruction_data.clone(), + pre_states, + pda_seeds: vec![], + }; + + let mut chained_calls = VecDeque::from_iter([(initial_call, program)]); + let mut chain_calls_counter = 0; + while let Some((chained_call, program)) = chained_calls.pop_front() { + if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS { + return Err(NssaError::MaxChainedCallsDepthExceeded); + } + + let inner_receipt = execute_and_prove_program( + program, + &chained_call.pre_states, + &chained_call.instruction_data, + )?; let program_output: ProgramOutput = inner_receipt .journal @@ -76,39 +93,23 @@ pub fn execute_and_prove( // Prove circuit. env_builder.add_assumption(inner_receipt); - // TODO: Remove when multi-chain calls are supported in the circuit - assert!(program_output.chained_calls.len() <= 1); - // TODO: Modify when multi-chain calls are supported in the circuit - if let Some(next_call) = program_output.chained_calls.first() { - program = dependencies - .get(&next_call.program_id) + for new_call in program_output.chained_calls.into_iter().rev() { + let next_program = dependencies + .get(&new_call.program_id) .ok_or(NssaError::InvalidProgramBehavior)?; - instruction_data = next_call.instruction_data.clone(); - // Build post states with metadata for next call - let mut post_states_with_metadata = Vec::new(); - 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.account().clone(); - post_states_with_metadata.push(post_with_metadata); - } - - pre_states = next_call.pre_states.clone(); - } else { - break; + chained_calls.push_front((new_call, next_program)); } + + chain_calls_counter += 1; } let circuit_input = PrivacyPreservingCircuitInput { program_outputs, - visibility_mask: visibility_mask.to_vec(), - private_account_nonces: private_account_nonces.to_vec(), - private_account_keys: private_account_keys.to_vec(), - private_account_nsks: private_account_nsks.to_vec(), - private_account_membership_proofs: private_account_membership_proofs.to_vec(), + visibility_mask, + private_account_nonces, + private_account_keys, + private_account_nsks, + private_account_membership_proofs, program_id: program_with_dependencies.program.id(), }; @@ -215,13 +216,13 @@ mod tests { let shared_secret = SharedSecretKey::new(&esk, &recipient_keys.ivk()); let (output, proof) = execute_and_prove( - &[sender, recipient], - &Program::serialize_instruction(balance_to_move).unwrap(), - &[0, 2], - &[0xdeadbeef], - &[(recipient_keys.npk(), shared_secret)], - &[], - &[None], + vec![sender, recipient], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![0, 2], + vec![0xdeadbeef], + vec![(recipient_keys.npk(), shared_secret)], + vec![], + vec![None], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -311,16 +312,16 @@ mod tests { let shared_secret_2 = SharedSecretKey::new(&esk_2, &recipient_keys.ivk()); let (output, proof) = execute_and_prove( - &[sender_pre.clone(), recipient], - &Program::serialize_instruction(balance_to_move).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![sender_pre.clone(), recipient], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ (sender_keys.npk(), shared_secret_1), (recipient_keys.npk(), shared_secret_2), ], - &[sender_keys.nsk], - &[commitment_set.get_proof_for(&commitment_sender), None], + vec![sender_keys.nsk], + vec![commitment_set.get_proof_for(&commitment_sender), None], &program.into(), ) .unwrap(); diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 69cb02c5..943b16ed 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -226,6 +226,15 @@ mod tests { } } + pub fn changer_claimer() -> Self { + use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID}; + + Program { + id: CHANGER_CLAIMER_ID, + elf: CHANGER_CLAIMER_ELF.to_vec(), + } + } + pub fn noop() -> Self { use test_program_methods::{NOOP_ELF, NOOP_ID}; @@ -235,6 +244,17 @@ mod tests { } } + pub fn malicious_authorization_changer() -> Self { + use test_program_methods::{ + MALICIOUS_AUTHORIZATION_CHANGER_ELF, MALICIOUS_AUTHORIZATION_CHANGER_ID, + }; + + Program { + id: MALICIOUS_AUTHORIZATION_CHANGER_ID, + elf: MALICIOUS_AUTHORIZATION_CHANGER_ELF.to_vec(), + } + } + pub fn modified_transfer_program() -> Self { use test_program_methods::MODIFIED_TRANSFER_ELF; // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 68437e46..f5badb6a 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use log::debug; use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, - program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution}, + program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; @@ -119,7 +119,7 @@ impl PublicTransaction { return Err(NssaError::MaxChainedCallsDepthExceeded); } - // Check the `program_id` corresponds to a deployed program + // Check that the `program_id` corresponds to a deployed program let Some(program) = state.programs().get(&chained_call.program_id) else { return Err(NssaError::InvalidInput("Unknown program".into())); }; @@ -135,12 +135,14 @@ impl PublicTransaction { chained_call.program_id, program_output ); - let authorized_pdas = - self.compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds); + let authorized_pdas = nssa_core::program::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 + // Check that the program output pre_states coincide 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) @@ -198,22 +200,23 @@ impl PublicTransaction { chain_calls_counter += 1; } - 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() + // Check that all modified uninitialized accounts where claimed + for post in state_diff.iter().filter_map(|(account_id, post)| { + let pre = state.get_account_by_id(account_id); + if pre.program_owner != DEFAULT_PROGRAM_ID { + return None; + } + if pre == *post { + return None; + } + Some(post) + }) { + if post.program_owner == DEFAULT_PROGRAM_ID { + return Err(NssaError::InvalidProgramBehavior); + } } + + Ok(state_diff) } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index bc0ff62e..1a384b2f 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -504,6 +504,7 @@ pub mod tests { self.insert_program(Program::chain_caller()); self.insert_program(Program::amm()); self.insert_program(Program::claimer()); + self.insert_program(Program::changer_claimer()); self } @@ -865,13 +866,13 @@ pub mod tests { let epk = EphemeralPublicKey::from_scalar(esk); let (output, proof) = circuit::execute_and_prove( - &[sender, recipient], - &Program::serialize_instruction(balance_to_move).unwrap(), - &[0, 2], - &[0xdeadbeef], - &[(recipient_keys.npk(), shared_secret)], - &[], - &[None], + vec![sender, recipient], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![0, 2], + vec![0xdeadbeef], + vec![(recipient_keys.npk(), shared_secret)], + vec![], + vec![None], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -912,16 +913,16 @@ pub mod tests { let epk_2 = EphemeralPublicKey::from_scalar(esk_2); let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &Program::serialize_instruction(balance_to_move).unwrap(), - &[1, 2], - &new_nonces, - &[ + vec![sender_pre, recipient_pre], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![1, 2], + new_nonces.to_vec(), + vec![ (sender_keys.npk(), shared_secret_1), (recipient_keys.npk(), shared_secret_2), ], - &[sender_keys.nsk], - &[state.get_proof_for_commitment(&sender_commitment), None], + vec![sender_keys.nsk], + vec![state.get_proof_for_commitment(&sender_commitment), None], &program.into(), ) .unwrap(); @@ -965,13 +966,13 @@ pub mod tests { let epk = EphemeralPublicKey::from_scalar(esk); let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &Program::serialize_instruction(balance_to_move).unwrap(), - &[1, 0], - &[new_nonce], - &[(sender_keys.npk(), shared_secret)], - &[sender_keys.nsk], - &[state.get_proof_for_commitment(&sender_commitment)], + vec![sender_pre, recipient_pre], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![1, 0], + vec![new_nonce], + vec![(sender_keys.npk(), shared_secret)], + vec![sender_keys.nsk], + vec![state.get_proof_for_commitment(&sender_commitment)], &program.into(), ) .unwrap(); @@ -1179,13 +1180,13 @@ pub mod tests { ); let result = execute_and_prove( - &[public_account], - &Program::serialize_instruction(10u128).unwrap(), - &[0], - &[], - &[], - &[], - &[], + vec![public_account], + Program::serialize_instruction(10u128).unwrap(), + vec![0], + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1206,13 +1207,13 @@ pub mod tests { ); let result = execute_and_prove( - &[public_account], - &Program::serialize_instruction(10u128).unwrap(), - &[0], - &[], - &[], - &[], - &[], + vec![public_account], + Program::serialize_instruction(10u128).unwrap(), + vec![0], + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1233,13 +1234,13 @@ pub mod tests { ); let result = execute_and_prove( - &[public_account], - &Program::serialize_instruction(()).unwrap(), - &[0], - &[], - &[], - &[], - &[], + vec![public_account], + Program::serialize_instruction(()).unwrap(), + vec![0], + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1260,13 +1261,13 @@ pub mod tests { ); let result = execute_and_prove( - &[public_account], - &Program::serialize_instruction(vec![0]).unwrap(), - &[0], - &[], - &[], - &[], - &[], + vec![public_account], + Program::serialize_instruction(vec![0]).unwrap(), + vec![0], + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1289,13 +1290,13 @@ pub mod tests { let large_data: Vec = vec![0; nssa_core::account::data::DATA_MAX_LENGTH_IN_BYTES + 1]; let result = execute_and_prove( - &[public_account], - &Program::serialize_instruction(large_data).unwrap(), - &[0], - &[], - &[], - &[], - &[], + vec![public_account], + Program::serialize_instruction(large_data).unwrap(), + vec![0], + vec![], + vec![], + vec![], + vec![], &program.to_owned().into(), ); @@ -1316,13 +1317,13 @@ pub mod tests { ); let result = execute_and_prove( - &[public_account], - &Program::serialize_instruction(()).unwrap(), - &[0], - &[], - &[], - &[], - &[], + vec![public_account], + Program::serialize_instruction(()).unwrap(), + vec![0], + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1352,13 +1353,13 @@ pub mod tests { ); let result = execute_and_prove( - &[public_account_1, public_account_2], - &Program::serialize_instruction(()).unwrap(), - &[0, 0], - &[], - &[], - &[], - &[], + vec![public_account_1, public_account_2], + Program::serialize_instruction(()).unwrap(), + vec![0, 0], + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1379,13 +1380,13 @@ pub mod tests { ); let result = execute_and_prove( - &[public_account], - &Program::serialize_instruction(()).unwrap(), - &[0], - &[], - &[], - &[], - &[], + vec![public_account], + Program::serialize_instruction(()).unwrap(), + vec![0], + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1415,13 +1416,13 @@ pub mod tests { ); let result = execute_and_prove( - &[public_account_1, public_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[0, 0], - &[], - &[], - &[], - &[], + vec![public_account_1, public_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![0, 0], + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1453,13 +1454,13 @@ pub mod tests { // Setting only one visibility mask for a circuit execution with two pre_state accounts. let visibility_mask = [0]; let result = execute_and_prove( - &[public_account_1, public_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &visibility_mask, - &[], - &[], - &[], - &[], + vec![public_account_1, public_account_2], + Program::serialize_instruction(10u128).unwrap(), + visibility_mask.to_vec(), + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1486,11 +1487,11 @@ pub mod tests { // Setting only one nonce for an execution with two private accounts. let private_account_nonces = [0xdeadbeef1]; let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &private_account_nonces, - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + private_account_nonces.to_vec(), + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1500,8 +1501,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -1530,13 +1531,13 @@ pub mod tests { SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), )]; let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &private_account_keys, - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + private_account_keys.to_vec(), + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -1563,11 +1564,11 @@ pub mod tests { // Setting no second commitment proof. let private_account_membership_proofs = [Some((0, vec![]))]; let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1577,8 +1578,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[sender_keys.nsk], - &private_account_membership_proofs, + vec![sender_keys.nsk], + private_account_membership_proofs.to_vec(), &program.into(), ); @@ -1605,11 +1606,11 @@ pub mod tests { // Setting no auth key for an execution with one non default private accounts. let private_account_nsks = []; let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1619,8 +1620,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &private_account_nsks, - &[], + private_account_nsks.to_vec(), + vec![], &program.into(), ); @@ -1663,13 +1664,13 @@ pub mod tests { let private_account_nsks = [recipient_keys.nsk]; let private_account_membership_proofs = [Some((0, vec![]))]; let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &private_account_keys, - &private_account_nsks, - &private_account_membership_proofs, + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + private_account_keys.to_vec(), + private_account_nsks.to_vec(), + private_account_membership_proofs.to_vec(), &program.into(), ); @@ -1701,11 +1702,11 @@ pub mod tests { ); let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1715,8 +1716,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -1749,11 +1750,11 @@ pub mod tests { ); let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1763,8 +1764,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -1796,11 +1797,11 @@ pub mod tests { ); let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1810,8 +1811,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -1843,11 +1844,11 @@ pub mod tests { ); let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1857,8 +1858,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -1888,11 +1889,11 @@ pub mod tests { ); let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1902,8 +1903,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -1927,13 +1928,13 @@ pub mod tests { let visibility_mask = [0, 3]; let result = execute_and_prove( - &[public_account_1, public_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &visibility_mask, - &[], - &[], - &[], - &[], + vec![public_account_1, public_account_2], + Program::serialize_instruction(10u128).unwrap(), + visibility_mask.to_vec(), + vec![], + vec![], + vec![], + vec![], &program.into(), ); @@ -1961,11 +1962,11 @@ pub mod tests { // accounts. let private_account_nonces = [0xdeadbeef1, 0xdeadbeef2, 0xdeadbeef3]; let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &private_account_nonces, - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + private_account_nonces.to_vec(), + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -1975,8 +1976,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -2017,13 +2018,13 @@ pub mod tests { ), ]; let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &[1, 2], - &[0xdeadbeef1, 0xdeadbeef2], - &private_account_keys, - &[sender_keys.nsk], - &[Some((0, vec![]))], + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + vec![1, 2], + vec![0xdeadbeef1, 0xdeadbeef2], + private_account_keys.to_vec(), + vec![sender_keys.nsk], + vec![Some((0, vec![]))], &program.into(), ); @@ -2053,11 +2054,11 @@ pub mod tests { let private_account_nsks = [sender_keys.nsk, recipient_keys.nsk]; let private_account_membership_proofs = [Some((0, vec![])), Some((1, vec![]))]; let result = execute_and_prove( - &[private_account_1, private_account_2], - &Program::serialize_instruction(10u128).unwrap(), - &visibility_mask, - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1, private_account_2], + Program::serialize_instruction(10u128).unwrap(), + visibility_mask.to_vec(), + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ ( sender_keys.npk(), SharedSecretKey::new(&[55; 32], &sender_keys.ivk()), @@ -2067,8 +2068,8 @@ pub mod tests { SharedSecretKey::new(&[56; 32], &recipient_keys.ivk()), ), ], - &private_account_nsks, - &private_account_membership_proofs, + private_account_nsks.to_vec(), + private_account_membership_proofs.to_vec(), &program.into(), ); @@ -2149,16 +2150,16 @@ pub mod tests { let private_account_membership_proofs = [Some((1, vec![])), Some((1, vec![]))]; let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.ivk()); let result = execute_and_prove( - &[private_account_1.clone(), private_account_1], - &Program::serialize_instruction(100u128).unwrap(), - &visibility_mask, - &[0xdeadbeef1, 0xdeadbeef2], - &[ + vec![private_account_1.clone(), private_account_1], + Program::serialize_instruction(100u128).unwrap(), + visibility_mask.to_vec(), + vec![0xdeadbeef1, 0xdeadbeef2], + vec![ (sender_keys.npk(), shared_secret), (sender_keys.npk(), shared_secret), ], - &private_account_nsks, - &private_account_membership_proofs, + private_account_nsks.to_vec(), + private_account_membership_proofs.to_vec(), &program.into(), ); @@ -3941,8 +3942,9 @@ pub mod tests { assert_eq!(to_post, expected_to_post); } - #[test] - fn test_private_chained_call() { + #[test_case::test_case(1; "single call")] + #[test_case::test_case(2; "two calls")] + fn test_private_chained_call(number_of_calls: u32) { // Arrange let chain_caller = Program::chain_caller(); let auth_transfers = Program::authenticated_transfer_program(); @@ -3978,7 +3980,7 @@ pub mod tests { let instruction: (u128, ProgramId, u32, Option) = ( amount, Program::authenticated_transfer_program().id(), - 1, + number_of_calls, None, ); @@ -3999,14 +4001,14 @@ pub mod tests { let to_new_nonce = 0xdeadbeef2; let from_expected_post = Account { - balance: initial_balance - amount, + balance: initial_balance - number_of_calls as u128 * amount, nonce: from_new_nonce, ..from_account.account.clone() }; let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post); let to_expected_post = Account { - balance: amount, + balance: number_of_calls as u128 * amount, nonce: to_new_nonce, ..to_account.account.clone() }; @@ -4014,13 +4016,13 @@ pub mod tests { // Act let (output, proof) = execute_and_prove( - &[to_account, from_account], - &Program::serialize_instruction(instruction).unwrap(), - &[1, 1], - &[from_new_nonce, to_new_nonce], - &[(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)], - &[from_keys.nsk, to_keys.nsk], - &[ + vec![to_account, from_account], + Program::serialize_instruction(instruction).unwrap(), + vec![1, 1], + vec![from_new_nonce, to_new_nonce], + vec![(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)], + vec![from_keys.nsk, to_keys.nsk], + vec![ state.get_proof_for_commitment(&from_commitment), state.get_proof_for_commitment(&to_commitment), ], @@ -4255,13 +4257,13 @@ pub mod tests { // Execute and prove the circuit with the authorized account but no commitment proof let (output, proof) = execute_and_prove( - std::slice::from_ref(&authorized_account), - &Program::serialize_instruction(balance).unwrap(), - &[1], - &[nonce], - &[(private_keys.npk(), shared_secret)], - &[private_keys.nsk], - &[None], + vec![authorized_account], + Program::serialize_instruction(balance).unwrap(), + vec![1], + vec![nonce], + vec![(private_keys.npk(), shared_secret)], + vec![private_keys.nsk], + vec![None], &program.into(), ) .unwrap(); @@ -4308,13 +4310,13 @@ pub mod tests { // Step 2: Execute claimer program to claim the account with authentication let (output, proof) = execute_and_prove( - std::slice::from_ref(&authorized_account), - &Program::serialize_instruction(balance).unwrap(), - &[1], - &[nonce], - &[(private_keys.npk(), shared_secret)], - &[private_keys.nsk], - &[None], + vec![authorized_account.clone()], + Program::serialize_instruction(balance).unwrap(), + vec![1], + vec![nonce], + vec![(private_keys.npk(), shared_secret)], + vec![private_keys.nsk], + vec![None], &claimer_program.into(), ) .unwrap(); @@ -4356,16 +4358,174 @@ pub mod tests { // Step 3: Try to execute noop program with authentication but without initialization let res = execute_and_prove( - std::slice::from_ref(&account_metadata), - &Program::serialize_instruction(()).unwrap(), - &[1], - &[nonce2], - &[(private_keys.npk(), shared_secret2)], - &[private_keys.nsk], - &[None], + vec![account_metadata], + Program::serialize_instruction(()).unwrap(), + vec![1], + vec![nonce2], + vec![(private_keys.npk(), shared_secret2)], + vec![private_keys.nsk], + vec![None], &noop_program.into(), ); assert!(matches!(res, Err(NssaError::CircuitProvingError(_)))); } + + #[test] + fn test_public_changer_claimer_no_data_change_no_claim_succeeds() { + let initial_data = []; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let account_id = AccountId::new([1; 32]); + let program_id = Program::changer_claimer().id(); + // Don't change data (None) and don't claim (false) + let instruction: (Option>, bool) = (None, false); + + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction) + .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); + + // Should succeed - no changes made, no claim needed + assert!(result.is_ok()); + // Account should remain default/unclaimed + assert_eq!(state.get_account_by_id(&account_id), Account::default()); + } + + #[test] + fn test_public_changer_claimer_data_change_no_claim_fails() { + let initial_data = []; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let account_id = AccountId::new([1; 32]); + let program_id = Program::changer_claimer().id(); + // Change data but don't claim (false) - should fail + let new_data = vec![1, 2, 3, 4, 5]; + let instruction: (Option>, bool) = (Some(new_data), false); + + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction) + .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); + + // Should fail - cannot modify data without claiming the account + assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + } + + #[test] + fn test_private_changer_claimer_no_data_change_no_claim_succeeds() { + let program = Program::changer_claimer(); + let sender_keys = test_private_account_keys_1(); + let private_account = + AccountWithMetadata::new(Account::default(), true, &sender_keys.npk()); + // Don't change data (None) and don't claim (false) + let instruction: (Option>, bool) = (None, false); + + let result = execute_and_prove( + vec![private_account], + Program::serialize_instruction(instruction).unwrap(), + vec![1], + vec![2], + vec![( + sender_keys.npk(), + SharedSecretKey::new(&[3; 32], &sender_keys.ivk()), + )], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], + &program.into(), + ); + + // Should succeed - no changes made, no claim needed + assert!(result.is_ok()); + } + + #[test] + fn test_private_changer_claimer_data_change_no_claim_fails() { + let program = Program::changer_claimer(); + let sender_keys = test_private_account_keys_1(); + let private_account = + AccountWithMetadata::new(Account::default(), true, &sender_keys.npk()); + // Change data but don't claim (false) - should fail + let new_data = vec![1, 2, 3, 4, 5]; + let instruction: (Option>, bool) = (Some(new_data), false); + + let result = execute_and_prove( + vec![private_account], + Program::serialize_instruction(instruction).unwrap(), + vec![1], + vec![2], + vec![( + sender_keys.npk(), + SharedSecretKey::new(&[3; 32], &sender_keys.ivk()), + )], + vec![sender_keys.nsk], + vec![Some((0, vec![]))], + &program.into(), + ); + + // Should fail - cannot modify data without claiming the account + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } + + #[test] + fn test_malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() { + // Arrange + let malicious_program = Program::malicious_authorization_changer(); + let auth_transfers = Program::authenticated_transfer_program(); + let sender_keys = test_public_account_keys_1(); + let recipient_keys = test_private_account_keys_1(); + + let sender_account = AccountWithMetadata::new( + Account { + program_owner: auth_transfers.id(), + balance: 100, + ..Default::default() + }, + false, + sender_keys.account_id(), + ); + let recipient_account = + AccountWithMetadata::new(Account::default(), true, &recipient_keys.npk()); + + let recipient_commitment = + Commitment::new(&recipient_keys.npk(), &recipient_account.account); + let state = V02State::new_with_genesis_accounts( + &[(sender_account.account_id, sender_account.account.balance)], + std::slice::from_ref(&recipient_commitment), + ) + .with_test_programs(); + + let balance_to_transfer = 10u128; + let instruction = (balance_to_transfer, auth_transfers.id()); + + let recipient_esk = [3; 32]; + let recipient = SharedSecretKey::new(&recipient_esk, &recipient_keys.ivk()); + + let mut dependencies = HashMap::new(); + dependencies.insert(auth_transfers.id(), auth_transfers); + let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies); + + let recipient_new_nonce = 0xdeadbeef1; + + // Act - execute the malicious program - this should fail during proving + let result = execute_and_prove( + vec![sender_account, recipient_account], + Program::serialize_instruction(instruction).unwrap(), + vec![0, 1], + vec![recipient_new_nonce], + vec![(recipient_keys.npk(), recipient)], + vec![recipient_keys.nsk], + vec![state.get_proof_for_commitment(&recipient_commitment)], + &program_with_deps, + ); + + // Assert - should fail because the malicious program tries to manipulate is_authorized + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } } diff --git a/program_methods/guest/src/bin/pinata.rs b/program_methods/guest/src/bin/pinata.rs index a0c46a1a..0dc3c108 100644 --- a/program_methods/guest/src/bin/pinata.rs +++ b/program_methods/guest/src/bin/pinata.rs @@ -77,7 +77,7 @@ fn main() { instruction_words, vec![pinata, winner], vec![ - AccountPostState::new(pinata_post), + AccountPostState::new_claimed_if_default(pinata_post), AccountPostState::new(winner_post), ], ); diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index ffe4b130..4bbd895f 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,12 +1,18 @@ -use std::collections::HashMap; +use std::{ + collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + convert::Infallible, +}; use nssa_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, - NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, - account::{Account, AccountId, AccountWithMetadata}, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof, + Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, + PrivacyPreservingCircuitOutput, SharedSecretKey, + account::{Account, AccountId, AccountWithMetadata, Nonce}, compute_digest_for_path, - encryption::Ciphertext, - program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, validate_execution}, + program::{ + AccountPostState, ChainedCall, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramId, + ProgramOutput, validate_execution, + }, }; use risc0_zkvm::{guest::env, serde::to_vec}; @@ -18,118 +24,224 @@ fn main() { private_account_keys, private_account_nsks, private_account_membership_proofs, - mut program_id, + program_id, } = env::read(); - let mut pre_states: Vec = Vec::new(); - let mut state_diff: HashMap = HashMap::new(); + let execution_state = ExecutionState::derive_from_outputs(program_id, program_outputs); - let num_calls = program_outputs.len(); - if num_calls > MAX_NUMBER_CHAINED_CALLS { - panic!("Max chained calls depth is exceeded"); - } + let output = compute_circuit_output( + execution_state, + &visibility_mask, + &private_account_nonces, + &private_account_keys, + &private_account_nsks, + &private_account_membership_proofs, + ); - let Some(last_program_call) = program_outputs.last() else { - panic!("Program outputs is empty") - }; + env::commit(&output); +} - if !last_program_call.chained_calls.is_empty() { - panic!("Call stack is incomplete"); - } +/// State of the involved accounts before and after program execution. +struct ExecutionState { + pre_states: Vec, + post_states: HashMap, +} - for window in program_outputs.windows(2) { - let caller = &window[0]; - let callee = &window[1]; - - if caller.chained_calls.len() > 1 { - panic!("Privacy Multi-chained calls are not supported yet"); - } - - // TODO: Modify when multi-chain calls are supported in the circuit - let Some(caller_chained_call) = &caller.chained_calls.first() else { - panic!("Expected chained call"); +impl ExecutionState { + /// Validate program outputs and derive the overall execution state. + pub fn derive_from_outputs(program_id: ProgramId, program_outputs: Vec) -> Self { + let Some(first_output) = program_outputs.first() else { + panic!("No program outputs provided"); }; - // Check that instruction data in caller is the instruction data in callee - if caller_chained_call.instruction_data != callee.instruction_data { - panic!("Invalid instruction data"); - } - - // Check that account pre_states in caller are the ones in calle - if caller_chained_call.pre_states != callee.pre_states { - panic!("Invalid pre states"); - } - } - - for (i, program_output) in program_outputs.iter().enumerate() { - let mut program_output = program_output.clone(); - - // Check that `program_output` is consistent with the execution of the corresponding - // program. - let program_output_words = - &to_vec(&program_output).expect("program_output must be serializable"); - env::verify(program_id, program_output_words) - .expect("program output must match the program's execution"); - - // Check that the program is well behaved. - // See the # Programs section for the definition of the `validate_execution` method. - if !validate_execution( - &program_output.pre_states, - &program_output.post_states, + let initial_call = ChainedCall { program_id, - ) { - panic!("Bad behaved program"); - } + instruction_data: first_output.instruction_data.clone(), + pre_states: first_output.pre_states.clone(), + pda_seeds: Vec::new(), + }; + let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); - // The invoked program claims the accounts with default program id. - 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 = program_id; - } else { - panic!("Cannot claim an initialized account") + let mut execution_state = ExecutionState { + pre_states: Vec::new(), + post_states: HashMap::new(), + }; + + let mut program_outputs_iter = program_outputs.into_iter(); + let mut chain_calls_counter = 0; + + while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() { + assert!( + chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS, + "Max chained calls depth is exceeded" + ); + + let Some(program_output) = program_outputs_iter.next() else { + panic!("Insufficient program outputs for chained calls"); + }; + + // Check that instruction data in chained call is the instruction data in program output + assert_eq!( + chained_call.instruction_data, program_output.instruction_data, + "Mismatched instruction data between chained call and program output" + ); + + // Check that `program_output` is consistent with the execution of the corresponding + // program. + let program_output_words = + &to_vec(&program_output).expect("program_output must be serializable"); + env::verify(chained_call.program_id, program_output_words).unwrap_or_else( + |_: Infallible| unreachable!("Infallible error is never constructed"), + ); + + // Check that the program is well behaved. + // See the # Programs section for the definition of the `validate_execution` method. + let execution_valid = validate_execution( + &program_output.pre_states, + &program_output.post_states, + chained_call.program_id, + ); + assert!(execution_valid, "Bad behaved program"); + + for next_call in program_output.chained_calls.iter().rev() { + chained_calls.push_front((next_call.clone(), Some(chained_call.program_id))); } + + let authorized_pdas = nssa_core::program::compute_authorized_pdas( + caller_program_id, + &chained_call.pda_seeds, + ); + execution_state.validate_and_sync_states( + chained_call.program_id, + authorized_pdas, + program_output.pre_states, + program_output.post_states, + ); + chain_calls_counter += 1; } - for (pre, post) in program_output + assert!( + program_outputs_iter.next().is_none(), + "Inner call without a chained call found", + ); + + // Check that all modified uninitialized accounts were claimed + for (account_id, post) in execution_state .pre_states .iter() - .zip(&program_output.post_states) + .filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID) + .map(|a| { + let post = execution_state + .post_states + .get(&a.account_id) + .expect("Post state must exist for pre state"); + (a, post) + }) + .filter(|(pre_default, post)| pre_default.account != **post) + .map(|(pre, post)| (pre.account_id, post)) { - if let Some(account_pre) = state_diff.get(&pre.account_id) { - if account_pre != &pre.account { - panic!("Invalid input"); - } - } else { - pre_states.push(pre.clone()); - } - state_diff.insert(pre.account_id, post.account().clone()); + assert_ne!( + post.program_owner, DEFAULT_PROGRAM_ID, + "Account {account_id:?} was modified but not claimed" + ); } - // TODO: Modify when multi-chain calls are supported in the circuit - if let Some(next_chained_call) = &program_output.chained_calls.first() { - program_id = next_chained_call.program_id; - } else if i != program_outputs.len() - 1 { - panic!("Inner call without a chained call found") - }; + execution_state } - let n_accounts = pre_states.len(); - if visibility_mask.len() != n_accounts { - panic!("Invalid visibility mask length"); + /// Validate program pre and post states and populate the execution state. + fn validate_and_sync_states( + &mut self, + program_id: ProgramId, + authorized_pdas: HashSet, + pre_states: Vec, + post_states: Vec, + ) { + for (pre, mut post) in pre_states.into_iter().zip(post_states) { + let pre_account_id = pre.account_id; + let post_states_entry = self.post_states.entry(pre.account_id); + match &post_states_entry { + Entry::Occupied(occupied) => { + // Ensure that new pre state is the same as known post state + assert_eq!( + occupied.get(), + &pre.account, + "Inconsistent pre state for account {pre_account_id:?}", + ); + + let previous_is_authorized = self + .pre_states + .iter() + .find(|acc| acc.account_id == pre_account_id) + .map(|acc| acc.is_authorized) + .unwrap_or_else(|| { + panic!( + "Pre state must exist in execution state for account {pre_account_id:?}", + ) + }); + + let is_authorized = + previous_is_authorized || authorized_pdas.contains(&pre_account_id); + + assert_eq!( + pre.is_authorized, is_authorized, + "Inconsistent authorization for account {pre_account_id:?}", + ); + } + Entry::Vacant(_) => { + self.pre_states.push(pre); + } + } + + if 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 = program_id; + } else { + panic!("Cannot claim an initialized account {pre_account_id:?}"); + } + } + + post_states_entry.insert_entry(post.into_account()); + } } - // These lists will be the public outputs of this circuit - // and will be populated next. - let mut public_pre_states: Vec = Vec::new(); - let mut public_post_states: Vec = Vec::new(); - let mut ciphertexts: Vec = Vec::new(); - let mut new_commitments: Vec = Vec::new(); - let mut new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)> = Vec::new(); + /// Get an iterator over pre and post states of each account involved in the execution. + pub fn into_states_iter( + mut self, + ) -> impl ExactSizeIterator { + self.pre_states.into_iter().map(move |pre| { + let post = self + .post_states + .remove(&pre.account_id) + .expect("Account from pre states should exist in state diff"); + (pre, post) + }) + } +} + +fn compute_circuit_output( + execution_state: ExecutionState, + visibility_mask: &[u8], + private_account_nonces: &[Nonce], + private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], + private_account_nsks: &[NullifierSecretKey], + private_account_membership_proofs: &[Option], +) -> PrivacyPreservingCircuitOutput { + let mut output = PrivacyPreservingCircuitOutput { + public_pre_states: Vec::new(), + public_post_states: Vec::new(), + ciphertexts: Vec::new(), + new_commitments: Vec::new(), + new_nullifiers: Vec::new(), + }; + + let states_iter = execution_state.into_states_iter(); + assert_eq!( + visibility_mask.len(), + states_iter.len(), + "Invalid visibility mask length" + ); let mut private_nonces_iter = private_account_nonces.iter(); let mut private_keys_iter = private_account_keys.iter(); @@ -137,141 +249,156 @@ fn main() { let mut private_membership_proofs_iter = private_account_membership_proofs.iter(); let mut output_index = 0; - for i in 0..n_accounts { - match visibility_mask[i] { + for (visibility_mask, (pre_state, post_state)) in + visibility_mask.iter().copied().zip(states_iter) + { + match visibility_mask { 0 => { // Public account - public_pre_states.push(pre_states[i].clone()); - - let mut post = state_diff.get(&pre_states[i].account_id).unwrap().clone(); - - if post.program_owner == DEFAULT_PROGRAM_ID { - // Claim account - post.program_owner = program_id; - } - public_post_states.push(post); + output.public_pre_states.push(pre_state); + output.public_post_states.push(post_state); } 1 | 2 => { - let new_nonce = private_nonces_iter.next().expect("Missing private nonce"); - let (npk, shared_secret) = private_keys_iter.next().expect("Missing keys"); + let Some((npk, shared_secret)) = private_keys_iter.next() else { + panic!("Missing private account key"); + }; - if AccountId::from(npk) != pre_states[i].account_id { - panic!("AccountId mismatch"); - } + assert_eq!( + AccountId::from(npk), + pre_state.account_id, + "AccountId mismatch" + ); - if visibility_mask[i] == 1 { + let new_nullifier = if visibility_mask == 1 { // Private account with authentication - let nsk = private_nsks_iter.next().expect("Missing nsk"); + + let Some(nsk) = private_nsks_iter.next() else { + panic!("Missing private account nullifier secret key"); + }; // Verify the nullifier public key - let expected_npk = NullifierPublicKey::from(nsk); - if &expected_npk != npk { - panic!("Nullifier public key mismatch"); - } + assert_eq!( + npk, + &NullifierPublicKey::from(nsk), + "Nullifier public key mismatch" + ); // Check pre_state authorization - if !pre_states[i].is_authorized { - panic!("Pre-state not authorized"); - } + assert!( + pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); - let membership_proof_opt = private_membership_proofs_iter - .next() - .expect("Missing membership proof"); - let (nullifier, set_digest) = membership_proof_opt - .as_ref() - .map(|membership_proof| { - // Compute commitment set digest associated with provided auth path - let commitment_pre = Commitment::new(npk, &pre_states[i].account); - let set_digest = - compute_digest_for_path(&commitment_pre, membership_proof); + let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { + panic!("Missing membership proof"); + }; - // Compute update nullifier - let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); - (nullifier, set_digest) - }) - .unwrap_or_else(|| { - if pre_states[i].account != Account::default() { - panic!("Found new private account with non default values."); - } - - // Compute initialization nullifier - let nullifier = Nullifier::for_account_initialization(npk); - (nullifier, DUMMY_COMMITMENT_HASH) - }); - new_nullifiers.push((nullifier, set_digest)); + compute_nullifier_and_set_digest( + membership_proof_opt.as_ref(), + &pre_state.account, + npk, + nsk, + ) } else { // Private account without authentication - if pre_states[i].account != Account::default() { - panic!("Found new private account with non default values."); - } - if pre_states[i].is_authorized { - panic!("Found new private account marked as authorized."); - } + assert_eq!( + pre_state.account, + Account::default(), + "Found new private account with non default values", + ); + + assert!( + !pre_state.is_authorized, + "Found new private account marked as authorized." + ); + + let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { + panic!("Missing membership proof"); + }; - let membership_proof_opt = private_membership_proofs_iter - .next() - .expect("Missing membership proof"); assert!( membership_proof_opt.is_none(), "Membership proof must be None for unauthorized accounts" ); + let nullifier = Nullifier::for_account_initialization(npk); - new_nullifiers.push((nullifier, DUMMY_COMMITMENT_HASH)); - } + (nullifier, DUMMY_COMMITMENT_HASH) + }; + output.new_nullifiers.push(new_nullifier); // Update post-state with new nonce - let mut post_with_updated_values = - state_diff.get(&pre_states[i].account_id).unwrap().clone(); - post_with_updated_values.nonce = *new_nonce; - - if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID { - // Claim account - post_with_updated_values.program_owner = program_id; - } + let mut post_with_updated_nonce = post_state; + let Some(new_nonce) = private_nonces_iter.next() else { + panic!("Missing private account nonce"); + }; + post_with_updated_nonce.nonce = *new_nonce; // Compute commitment - let commitment_post = Commitment::new(npk, &post_with_updated_values); + let commitment_post = Commitment::new(npk, &post_with_updated_nonce); // Encrypt and push post state let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_values, + &post_with_updated_nonce, shared_secret, &commitment_post, output_index, ); - new_commitments.push(commitment_post); - ciphertexts.push(encrypted_account); + output.new_commitments.push(commitment_post); + output.ciphertexts.push(encrypted_account); output_index += 1; } _ => panic!("Invalid visibility mask value"), } } - if private_nonces_iter.next().is_some() { - panic!("Too many nonces"); - } + assert!(private_nonces_iter.next().is_none(), "Too many nonces"); - if private_keys_iter.next().is_some() { - panic!("Too many private account keys"); - } + assert!( + private_keys_iter.next().is_none(), + "Too many private account keys" + ); - if private_nsks_iter.next().is_some() { - panic!("Too many private account authentication keys"); - } + assert!( + private_nsks_iter.next().is_none(), + "Too many private account nullifier secret keys" + ); - if private_membership_proofs_iter.next().is_some() { - panic!("Too many private account membership proofs"); - } + assert!( + private_membership_proofs_iter.next().is_none(), + "Too many private account membership proofs" + ); - let output = PrivacyPreservingCircuitOutput { - public_pre_states, - public_post_states, - ciphertexts, - new_commitments, - new_nullifiers, - }; - - env::commit(&output); + output +} + +fn compute_nullifier_and_set_digest( + membership_proof_opt: Option<&MembershipProof>, + pre_account: &Account, + npk: &NullifierPublicKey, + nsk: &NullifierSecretKey, +) -> (Nullifier, CommitmentSetDigest) { + membership_proof_opt + .as_ref() + .map(|membership_proof| { + // Compute commitment set digest associated with provided auth path + let commitment_pre = Commitment::new(npk, pre_account); + let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); + + // Compute update nullifier + let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); + (nullifier, set_digest) + }) + .unwrap_or_else(|| { + assert_eq!( + *pre_account, + Account::default(), + "Found new private account with non default values" + ); + + // Compute initialization nullifier + let nullifier = Nullifier::for_account_initialization(npk); + (nullifier, DUMMY_COMMITMENT_HASH) + }) } diff --git a/test_program_methods/guest/src/bin/changer_claimer.rs b/test_program_methods/guest/src/bin/changer_claimer.rs new file mode 100644 index 00000000..8d28a490 --- /dev/null +++ b/test_program_methods/guest/src/bin/changer_claimer.rs @@ -0,0 +1,38 @@ +use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs}; + +type Instruction = (Option>, bool); + +/// A program that optionally modifies the account data and optionally claims it. +fn main() { + let ( + ProgramInput { + pre_states, + instruction: (data_opt, should_claim), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let [pre] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let account_pre = &pre.account; + let mut account_post = account_pre.clone(); + + // Update data if provided + if let Some(data) = data_opt { + account_post.data = data + .try_into() + .expect("provided data should fit into data limit"); + } + + // Claim or not based on the boolean flag + let post_state = if should_claim { + AccountPostState::new_claimed(account_post) + } else { + AccountPostState::new(account_post) + }; + + write_nssa_outputs(instruction_words, vec![pre], vec![post_state]); +} diff --git a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs new file mode 100644 index 00000000..7dc0ac68 --- /dev/null +++ b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs @@ -0,0 +1,53 @@ +use nssa_core::{ + account::AccountWithMetadata, + program::{ + AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, + }, +}; +use risc0_zkvm::serde::to_vec; + +type Instruction = (u128, ProgramId); + +/// A malicious test program that attempts to change authorization status. +/// It accepts two accounts and executes a native token transfer program via chain call, +/// but sets the `is_authorized` field of the first account to true. +fn main() { + let ( + ProgramInput { + pre_states, + instruction: (balance, transfer_program_id), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let [sender, receiver] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + // Maliciously set is_authorized to true for the first account + let authorised_sender = AccountWithMetadata { + is_authorized: true, + ..sender.clone() + }; + + let instruction_data = to_vec(&balance).unwrap(); + + let chained_call = ChainedCall { + program_id: transfer_program_id, + instruction_data, + pre_states: vec![authorised_sender.clone(), receiver.clone()], + pda_seeds: vec![], + }; + + write_nssa_outputs_with_chained_call( + instruction_words, + vec![sender.clone(), receiver.clone()], + vec![ + AccountPostState::new(sender.account), + AccountPostState::new(receiver.account), + ], + vec![chained_call], + ); +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 7c3da737..45709d05 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -375,7 +375,7 @@ impl WalletCore { pub async fn send_privacy_preserving_tx( &self, accounts: Vec, - instruction_data: &InstructionData, + instruction_data: InstructionData, program: &ProgramWithDependencies, ) -> Result<(SendTxResponse, Vec), ExecutionFailureKind> { self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| { @@ -387,7 +387,7 @@ impl WalletCore { pub async fn send_privacy_preserving_tx_with_pre_check( &self, accounts: Vec, - instruction_data: &InstructionData, + instruction_data: InstructionData, program: &ProgramWithDependencies, tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, ) -> Result<(SendTxResponse, Vec), ExecutionFailureKind> { @@ -403,16 +403,16 @@ impl WalletCore { let private_account_keys = acc_manager.private_account_keys(); let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( - &pre_states, + pre_states, instruction_data, - acc_manager.visibility_mask(), - &produce_random_nonces(private_account_keys.len()), - &private_account_keys + acc_manager.visibility_mask().to_vec(), + produce_random_nonces(private_account_keys.len()), + private_account_keys .iter() .map(|keys| (keys.npk.clone(), keys.ssk)) .collect::>(), - &acc_manager.private_account_auth(), - &acc_manager.private_account_membership_proofs(), + acc_manager.private_account_auth(), + acc_manager.private_account_membership_proofs(), &program.to_owned(), ) .unwrap(); diff --git a/wallet/src/program_facades/native_token_transfer/deshielded.rs b/wallet/src/program_facades/native_token_transfer/deshielded.rs index df604c65..7b774595 100644 --- a/wallet/src/program_facades/native_token_transfer/deshielded.rs +++ b/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -19,7 +19,7 @@ impl NativeTokenTransfer<'_> { PrivacyPreservingAccount::PrivateOwned(from), PrivacyPreservingAccount::Public(to), ], - &instruction_data, + instruction_data, &program.into(), tx_pre_check, ) diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs index da98aed1..eabf1b60 100644 --- a/wallet/src/program_facades/native_token_transfer/private.rs +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -17,7 +17,7 @@ impl NativeTokenTransfer<'_> { self.0 .send_privacy_preserving_tx( vec![PrivacyPreservingAccount::PrivateOwned(from)], - &Program::serialize_instruction(instruction).unwrap(), + Program::serialize_instruction(instruction).unwrap(), &Program::authenticated_transfer_program().into(), ) .await @@ -46,7 +46,7 @@ impl NativeTokenTransfer<'_> { ipk: to_ipk, }, ], - &instruction_data, + instruction_data, &program.into(), tx_pre_check, ) @@ -73,7 +73,7 @@ impl NativeTokenTransfer<'_> { PrivacyPreservingAccount::PrivateOwned(from), PrivacyPreservingAccount::PrivateOwned(to), ], - &instruction_data, + instruction_data, &program.into(), tx_pre_check, ) diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index 6abd2d2c..85c8145c 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -20,7 +20,7 @@ impl NativeTokenTransfer<'_> { PrivacyPreservingAccount::Public(from), PrivacyPreservingAccount::PrivateOwned(to), ], - &instruction_data, + instruction_data, &program.into(), tx_pre_check, ) @@ -52,7 +52,7 @@ impl NativeTokenTransfer<'_> { ipk: to_ipk, }, ], - &instruction_data, + instruction_data, &program.into(), tx_pre_check, ) diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs index fdd5d700..6036a603 100644 --- a/wallet/src/program_facades/pinata.rs +++ b/wallet/src/program_facades/pinata.rs @@ -37,7 +37,7 @@ impl Pinata<'_> { PrivacyPreservingAccount::Public(pinata_account_id), PrivacyPreservingAccount::PrivateOwned(winner_account_id), ], - &nssa::program::Program::serialize_instruction(solution).unwrap(), + nssa::program::Program::serialize_instruction(solution).unwrap(), &nssa::program::Program::pinata().into(), ) .await diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index fc03a0ac..0d3f79d7 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -52,7 +52,7 @@ impl Token<'_> { PrivacyPreservingAccount::Public(definition_account_id), PrivacyPreservingAccount::PrivateOwned(supply_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -82,7 +82,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::Public(supply_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -112,7 +112,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(supply_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -180,7 +180,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(sender_account_id), PrivacyPreservingAccount::PrivateOwned(recipient_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -212,7 +212,7 @@ impl Token<'_> { ipk: recipient_ipk, }, ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -240,7 +240,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(sender_account_id), PrivacyPreservingAccount::Public(recipient_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -269,7 +269,7 @@ impl Token<'_> { PrivacyPreservingAccount::Public(sender_account_id), PrivacyPreservingAccount::PrivateOwned(recipient_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -302,7 +302,7 @@ impl Token<'_> { ipk: recipient_ipk, }, ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -365,7 +365,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(holder_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -393,7 +393,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::Public(holder_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -422,7 +422,7 @@ impl Token<'_> { PrivacyPreservingAccount::Public(definition_account_id), PrivacyPreservingAccount::PrivateOwned(holder_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -491,7 +491,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::PrivateOwned(holder_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -523,7 +523,7 @@ impl Token<'_> { ipk: holder_ipk, }, ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -551,7 +551,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(definition_account_id), PrivacyPreservingAccount::Public(holder_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -580,7 +580,7 @@ impl Token<'_> { PrivacyPreservingAccount::Public(definition_account_id), PrivacyPreservingAccount::PrivateOwned(holder_account_id), ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await @@ -613,7 +613,7 @@ impl Token<'_> { ipk: holder_ipk, }, ], - &instruction_data, + instruction_data, &Program::token().into(), ) .await