#![no_main] use arbitrary::{Arbitrary, Unstructured}; use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction}; use libfuzzer_sys::fuzz_target; use nssa::V03State; fuzz_target!(|data: &[u8]| { let mut u = Unstructured::new(data); // Generate a fuzz-driven initial state instead of always using the fixed // testnet genesis. This exposes state-dependent bugs that only manifest // with specific account shapes (e.g. zero balance, u128::MAX balance, or a // nonce at the wrap-around boundary). 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(); // Construct the initial state let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0); // Generate up to 8 transactions and apply them let n_txs: u8 = u8::arbitrary(&mut u).unwrap_or(0) % 8; for i in 0..n_txs { // Mix correlated transactions (referencing known fuzz accounts and // correctly signed) with random ones. Correlated transactions give // the fuzzer a direct path to successful state transitions; random ones // exercise the rejection and isolation paths. let tx_result = if bool::arbitrary(&mut u).unwrap_or(false) { arb_fuzz_native_transfer(&mut u, &fuzz_accs) } else { arbitrary_transaction(&mut u) }; let Ok(tx) = tx_result else { break; }; // Stateless gate: only attempt state transitions that pass stateless check let Ok(tx) = tx.transaction_stateless_check() else { continue; }; // Clone state before to detect state leakage on failure let state_snapshot = state.clone(); // Advance block_id and timestamp each iteration so the state machine // sees a realistic monotonically-increasing context. Using the same // block_id=1 / timestamp=0 for every tx hides bugs that only manifest // when the block context changes across a multi-transaction sequence. let block_id: u64 = 1 + u64::from(i); let timestamp: u64 = u64::from(i); let result = tx.execute_check_on_state(&mut state, block_id, timestamp); match result { Err(_) => { // INVARIANT: StateIsolationOnFailure — a rejected tx must leave // public account balances unchanged. for &(acc_id, _) in &init_accs { let bal_before = state_snapshot.get_account_by_id(acc_id).balance; let bal_after = state.get_account_by_id(acc_id).balance; assert_eq!( bal_before, bal_after, "INVARIANT VIOLATION [StateIsolationOnFailure]: balance changed \ despite tx rejection for account {:?}", acc_id ); } } Ok(applied_tx) => { // INVARIANT: BalanceConservation — total balance of known accounts // must be conserved on success. Catches double-credit and // token-inflation bugs — two transfer paths that each credit the // recipient without debiting the sender would inflate the total, but // neither the rejection check nor any other per-account check catches // it unless we compare the aggregate. let total_before: u128 = init_accs .iter() .map(|&(acc_id, _)| state_snapshot.get_account_by_id(acc_id).balance) .fold(0u128, u128::saturating_add); let total_after: u128 = init_accs .iter() .map(|&(acc_id, _)| state.get_account_by_id(acc_id).balance) .fold(0u128, u128::saturating_add); assert_eq!( total_before, total_after, "INVARIANT VIOLATION [BalanceConservation]: total balance of genesis \ accounts changed after successful transaction (double-credit / \ token-inflation bug)", ); // INVARIANT: ReplayRejection — the nonce is consumed on first // acceptance; replaying the identical transaction in the very next // block must be rejected. // `execute_check_on_state` returns the `ValidatedTransaction` on // success, so we can feed it back without re-validating. let replay_result = applied_tx.execute_check_on_state(&mut state, block_id + 1, timestamp + 1); assert!( replay_result.is_err(), "INVARIANT VIOLATION [ReplayRejection]: transaction accepted twice — \ nonce replay not prevented (first block_id={block_id}, replay \ block_id={})", block_id + 1, ); } } } });