diff --git a/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs b/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs new file mode 100644 index 0000000..5fc710f --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs @@ -0,0 +1,141 @@ +#![no_main] +//! Fuzz target: `validate_on_state` → `apply_state_diff` split path vs +//! `execute_check_on_state` direct path. +//! +//! The following code path is covered: +//! +//! ```text +//! validate_on_state(tx, state) → diff +//! state.apply_state_diff(diff) +//! ``` +//! +//! In particular, `apply_state_diff` performs a two-step operation: +//! +//! 1. Write accounts from `public_diff` into state. +//! 2. Increment nonces for every account in `signer_account_ids`. +//! +//! A bug in nonce-increment logic (wrong account ID, off-by-one, missing increment, or +//! double increment) would be caught. +//! +//! # Invariants +//! +//! 1. **SplitPathEquivalence** — for every known account (genesis ∪ diff-declared), +//! calling `validate_on_state` followed by `apply_state_diff` must produce +//! exactly the same account state as calling `execute_check_on_state` directly. +//! This covers balance, nonce, data, and program_owner fields simultaneously. +//! +//! 2. **NonceIncrementCorrectness** — specifically for signer accounts, the nonce +//! after the split path (`validate + apply`) must equal the nonce after the +//! direct path (`execute`). This is a stricter corollary of invariant 1 but +//! is called out explicitly because nonce integrity is critical for replay +//! prevention. + +use std::collections::HashSet; + +use arbitrary::{Arbitrary, Unstructured}; +use fuzz_props::arbitrary_types::ArbNSSATransaction; +use fuzz_props::generators::arbitrary_fuzz_state; +use libfuzzer_sys::fuzz_target; +use nssa::V03State; + +fuzz_target!(|data: &[u8]| { + let mut u = Unstructured::new(data); + + // Generate a fuzz-driven initial state. + let fuzz_accs = match arbitrary_fuzz_state(&mut u) { + Ok(accs) => accs, + Err(_) => return, + }; + let init_accs: Vec<(nssa::AccountId, u128)> = fuzz_accs + .iter() + .map(|a| (a.account_id, a.balance)) + .collect(); + + // Generate and stateless-check a transaction. + let tx_raw = match ArbNSSATransaction::arbitrary(&mut u) { + Ok(w) => w.0, + Err(_) => return, + }; + let Ok(tx) = tx_raw.transaction_stateless_check() else { + return; + }; + + let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0); + + // ── Split path: validate → apply ───────────────────────────────────────── + // `validate_on_state` borrows `tx`; the transaction is still usable after. + let validate_result = tx.validate_on_state(&state, 1, 0); + + let Ok(diff) = validate_result else { + // validate_on_state returned Err — the direct path should also return Err. + // (That invariant is already covered by fuzz_validate_execute_consistency.) + return; + }; + + // Capture the IDs declared in the diff before consuming it in apply_state_diff. + let diff_account_ids: Vec = + diff.public_diff().keys().copied().collect(); + + // Apply the validated diff to a clone of the original state. + let mut split_state = state.clone(); + split_state.apply_state_diff(diff); + + // ── Direct path: execute_check_on_state ─────────────────────────────────── + // This consumes `tx`; it must succeed because validate_on_state already did. + let mut exec_state = state.clone(); + let execute_result = tx.execute_check_on_state(&mut exec_state, 1, 0); + + let Ok(_) = execute_result else { + // Agreement between validate and execute is checked in + // fuzz_validate_execute_consistency; we skip here to avoid duplicate noise. + return; + }; + + // ── Invariant 1: SplitPathEquivalence ──────────────────────────────────── + // Collect all account IDs we know about: genesis accounts ∪ diff-declared. + let all_known_ids: HashSet = init_accs + .iter() + .map(|&(id, _)| id) + .chain(diff_account_ids.into_iter()) + .collect(); + + for acc_id in &all_known_ids { + let split_account = split_state.get_account_by_id(*acc_id); + let exec_account = exec_state.get_account_by_id(*acc_id); + + assert_eq!( + split_account.balance, + exec_account.balance, + "INVARIANT VIOLATION [SplitPathEquivalence]: balance diverges for account {:?} \ + — split path balance={} vs execute path balance={}", + acc_id, + split_account.balance, + exec_account.balance, + ); + + // ── Invariant 2: NonceIncrementCorrectness ──────────────────────────── + assert_eq!( + split_account.nonce, + exec_account.nonce, + "INVARIANT VIOLATION [NonceIncrementCorrectness]: nonce diverges for account {:?} \ + after validate+apply vs execute — split path nonce={:?} vs execute path nonce={:?}", + acc_id, + split_account.nonce, + exec_account.nonce, + ); + + assert_eq!( + split_account.data, + exec_account.data, + "INVARIANT VIOLATION [SplitPathEquivalence]: data field diverges for account {:?}", + acc_id, + ); + + assert_eq!( + split_account.program_owner, + exec_account.program_owner, + "INVARIANT VIOLATION [SplitPathEquivalence]: program_owner diverges for account {:?}", + acc_id, + ); + } +});