2026-04-13 16:03:20 +08:00
|
|
|
#![no_main]
|
|
|
|
|
|
|
|
|
|
use arbitrary::{Arbitrary, Unstructured};
|
2026-05-12 13:18:18 +08:00
|
|
|
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
2026-04-13 16:03:20 +08:00
|
|
|
use libfuzzer_sys::fuzz_target;
|
|
|
|
|
use nssa::V03State;
|
|
|
|
|
|
|
|
|
|
fuzz_target!(|data: &[u8]| {
|
|
|
|
|
let mut u = Unstructured::new(data);
|
|
|
|
|
|
2026-05-12 13:18:18 +08:00
|
|
|
// 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
|
2026-04-13 16:03:20 +08:00
|
|
|
.iter()
|
|
|
|
|
.map(|a| (a.account_id, a.balance))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Construct the initial state
|
2026-04-15 13:47:36 +08:00
|
|
|
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
2026-04-13 16:03:20 +08:00
|
|
|
|
|
|
|
|
// Generate up to 8 transactions and apply them
|
|
|
|
|
let n_txs: u8 = u8::arbitrary(&mut u).unwrap_or(0) % 8;
|
2026-04-24 12:04:20 +08:00
|
|
|
for i in 0..n_txs {
|
2026-05-12 13:18:18 +08:00
|
|
|
// 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 {
|
2026-04-13 16:03:20 +08:00
|
|
|
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();
|
|
|
|
|
|
2026-04-24 12:04:20 +08:00
|
|
|
// 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);
|
2026-04-13 16:03:20 +08:00
|
|
|
let result = tx.execute_check_on_state(&mut state, block_id, timestamp);
|
|
|
|
|
|
|
|
|
|
if result.is_err() {
|
|
|
|
|
// INVARIANT: 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: balance changed despite tx rejection for account {:?}",
|
|
|
|
|
acc_id
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-12 13:00:38 +08:00
|
|
|
} else {
|
|
|
|
|
// INVARIANT: 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: total balance of genesis accounts changed after successful \
|
|
|
|
|
transaction (possible double-credit or token-inflation bug)",
|
|
|
|
|
);
|
2026-04-13 16:03:20 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|