diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 6865e5bd..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 0bf19fcb..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 b249f8d4..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 c058ec64..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 8b403494..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 ad5f4914..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 f73e61d1..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 b6315840..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 a409f3a6..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 671c8b1b..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 96331956..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 index ce20bff7..8f80ab58 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin 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 212482e6..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 e5046c27..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 3f6314dc..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 5fa4365f..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 f4f1b9a7..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 f39b705b..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 9a63dc0c..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/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/nssa/core/src/program.rs b/nssa/core/src/program.rs index bd9883fd..32b3e2c0 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -107,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 { diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 5673437d..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}; diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 91019c46..f5badb6a 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -200,6 +200,22 @@ impl PublicTransaction { chain_calls_counter += 1; } + // 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 88b2b187..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 } @@ -4370,6 +4371,108 @@ pub mod tests { 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 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 936c7e7d..4bbd895f 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -41,7 +41,7 @@ fn main() { env::commit(&output); } -/// World state before and after program execution. +/// State of the involved accounts before and after program execution. struct ExecutionState { pre_states: Vec, post_states: HashMap, @@ -66,9 +66,10 @@ impl ExecutionState { pre_states: Vec::new(), post_states: HashMap::new(), }; - let mut last_program_id = program_id; + 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, @@ -116,7 +117,6 @@ impl ExecutionState { program_output.pre_states, program_output.post_states, ); - last_program_id = chained_call.program_id; chain_calls_counter += 1; } @@ -125,11 +125,25 @@ impl ExecutionState { "Inner call without a chained call found", ); - // Claim accounts which were not explicitly claimed during execution - for account in execution_state.post_states.values_mut() { - if account.program_owner == DEFAULT_PROGRAM_ID { - account.program_owner = last_program_id; - } + // Check that all modified uninitialized accounts were claimed + for (account_id, post) in execution_state + .pre_states + .iter() + .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)) + { + assert_ne!( + post.program_owner, DEFAULT_PROGRAM_ID, + "Account {account_id:?} was modified but not claimed" + ); } execution_state 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]); +}