From 1ada67725bbb95008c3734d0ac3d8d3814c9177b Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:41:12 -0500 Subject: [PATCH] bad transaction test --- .../guest/src/bin/modified_transfer.rs | 71 ++++++++++++++ nssa/src/program.rs | 9 +- nssa/src/state.rs | 95 +++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 nssa/program_methods/guest/src/bin/modified_transfer.rs diff --git a/nssa/program_methods/guest/src/bin/modified_transfer.rs b/nssa/program_methods/guest/src/bin/modified_transfer.rs new file mode 100644 index 0000000..e09d204 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/modified_transfer.rs @@ -0,0 +1,71 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, +}; + +/// Initializes a default account under the ownership of this program. +/// This is achieved by a noop. +fn initialize_account(pre_state: AccountWithMetadata) { + let account_to_claim = pre_state.account.clone(); + let is_authorized = pre_state.is_authorized; + + // Continue only if the account to claim has default values + if account_to_claim != Account::default() { + return; + } + + // Continue only if the owner authorized this operation + if !is_authorized { + return; + } + + // Noop will result in account being claimed for this program + write_nssa_outputs(vec![pre_state], vec![account_to_claim]); +} + +/// Transfers `balance_to_move` native balance from `sender` to `recipient`. +fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) { + // Continue only if the sender has authorized this operation + if !sender.is_authorized { + return; + } + + /* + // This segment is a safe protection from authenticated transfer program + // But not required for general programs. + // Continue only if the sender has enough balance + if sender.account.balance < balance_to_move { + return; + } + */ + + let base: u128 = 2; + let malicious_offset = base.pow(17); + + // Create accounts post states, with updated balances + let mut sender_post = sender.account.clone(); + let mut recipient_post = recipient.account.clone(); + + sender_post.balance -= (balance_to_move + malicious_offset); + recipient_post.balance += balance_to_move + malicious_offset; + + write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); +} + +/// A transfer of balance program. +/// To be used both in public and private contexts. +fn main() { + // Read input accounts. + let ProgramInput { + pre_states, + instruction: balance_to_move, + } = read_nssa_inputs(); + + match (pre_states.as_slice(), balance_to_move) { + ([account_to_claim], 0) => initialize_account(account_to_claim.clone()), + ([sender, recipient], balance_to_move) => { + transfer(sender.clone(), recipient.clone(), balance_to_move) + } + _ => panic!("invalid params"), + } +} diff --git a/nssa/src/program.rs b/nssa/src/program.rs index d3f28b5..0db5451 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -7,7 +7,7 @@ use serde::Serialize; use crate::{ error::NssaError, - program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, + program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF, MODIFIED_TRANSFER_ELF}, }; /// Maximum number of cycles for a public execution. @@ -95,6 +95,13 @@ impl Program { // `program_methods` Self::new(TOKEN_ELF.to_vec()).unwrap() } + + pub fn modified_transfer_program() -> Self { + // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of + // `program_methods` + Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() + } + } // TODO: Testnet only. Refactor to prevent compilation on mainnet. diff --git a/nssa/src/state.rs b/nssa/src/state.rs index cef7791..d36c1c8 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2162,4 +2162,99 @@ pub mod tests { Err(NssaError::MaxChainedCallsDepthExceeded) )); } + + + #[test] + fn test_bad_transaction() { + + let sender_key = PrivateKey::try_new([37; 32]).unwrap(); + let sender_id = + AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let sender_init_balance: u128 = 10; + + let recipient_key = PrivateKey::try_new([42; 32]).unwrap(); + let recipient_id = + AccountId::from(&PublicKey::new_from_private_key(&recipient_key)); + let recipient_init_balance: u128 = 10; + + let mut state = + V02State::new_with_genesis_accounts(&[ + (sender_id.clone(), sender_init_balance), + (recipient_id.clone(), recipient_init_balance)], &[]); + + state.insert_program(Program::modified_transfer_program()); + + + let balance_to_move: u128 = 4; + + let sender = AccountWithMetadata::new( + state.get_account_by_id(&sender_id.clone()), + true, + sender_id.clone(), + ); + + let sender_nonce = sender.account.nonce; + + let recipient = AccountWithMetadata::new( + state.get_account_by_id(&recipient_id), + false, + sender_id.clone()); + + + let message = public_transaction::Message::try_new( + Program::modified_transfer_program().id(), + vec![sender_id, recipient_id], + vec![sender_nonce], + balance_to_move, + ) + .unwrap(); + + + let base: u128 = 2; + let malicious_offset = base.pow(17); + + let witness_set = + public_transaction::WitnessSet::for_message(&message, &[&sender_key]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let sender_post = state.get_account_by_id(&sender_id); + let recipient_post = state.get_account_by_id(&recipient_id); + // panic!("{}", sender_post.balance); + + let expected_sender_post = { + let mut this = state.get_account_by_id(&sender_id); + this.balance = sender_init_balance - balance_to_move - malicious_offset; + this.nonce = 1; + this + }; + + let expected_recipient_post = { + let mut this = state.get_account_by_id(&sender_id); + this.balance = recipient_init_balance + balance_to_move + malicious_offset; + this.nonce = 0; + this + }; + + assert!(expected_sender_post == sender_post); + assert!(expected_recipient_post == recipient_post); + + /* + let [expected_new_commitment] = tx.message().new_commitments.clone().try_into().unwrap(); + assert!(!state.private_state.0.contains(&expected_new_commitment)); + + state + .transition_from_privacy_preserving_transaction(&tx) + .unwrap(); + + let sender_post = state.get_account_by_id(&sender_keys.account_id()); + assert_eq!(sender_post, expected_sender_post); + assert!(state.private_state.0.contains(&expected_new_commitment)); + + assert_eq!( + state.get_account_by_id(&sender_keys.account_id()).balance, + 200 - balance_to_move + ); + */ + } }