lez-fuzzing/fuzz/fuzz_targets/fuzz_program_deployment_lifecycle.rs

111 lines
4.9 KiB
Rust

#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
//! Fuzz target: `V03State::transition_from_program_deployment_transaction`.
//!
//! The deployment path runs `ValidatedStateDiff::from_program_deployment_transaction`
//! followed by `apply_state_diff`, which means it touches WASM bytecode validation, program
//! ID derivation, and program insertion into `V03State::programs` — none of
//! which are exercised elsewhere.
//!
//! # Invariants
//!
//! 1. **NoPanic** — `transition_from_program_deployment_transaction` must never
//! panic on any arbitrary `ProgramDeploymentTransaction`, whether the bytecode
//! is valid WASM or random garbage. It must return `Ok` or `Err`.
//!
//! 2. **BalanceIsolation** — program deployment does not transfer or mint tokens.
//! Every genesis account balance must be identical before and after a
//! deployment call, regardless of whether the transaction succeeds or fails.
//!
//! 3. **StateIsolationOnFailure** — if the call returns `Err`, no genesis account
//! balance or nonce must change. This mirrors the `StateIsolationOnFailure`
//! invariant in `fuzz_state_transition` and ensures the deployment path shares
//! the same atomicity guarantee as the public-transaction path.
use arbitrary::{Arbitrary, Unstructured};
use fuzz_props::arbitrary_types::ArbProgramDeploymentTransaction;
use fuzz_props::generators::arbitrary_fuzz_state;
use nssa::V03State;
fuzz_props::fuzz_entry!(|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 an arbitrary program deployment transaction.
let tx_wrap = match ArbProgramDeploymentTransaction::arbitrary(&mut u) {
Ok(w) => w,
Err(_) => return,
};
let tx = tx_wrap.0;
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
// Capture per-account state snapshots before the deployment attempt.
let balances_before: Vec<u128> = init_accs
.iter()
.map(|&(id, _)| state.get_account_by_id(id).balance)
.collect();
let nonces_before: Vec<nssa_core::account::Nonce> = init_accs
.iter()
.map(|&(id, _)| state.get_account_by_id(id).nonce)
.collect();
// ── Invariant 1: NoPanic ──────────────────────────────────────────────────
// The call may return Ok or Err — it must not panic.
let result = state.transition_from_program_deployment_transaction(&tx);
match result {
Err(_) => {
// ── Invariant 3: StateIsolationOnFailure ──────────────────────────
// On failure, all genesis account balances and nonces must be unchanged.
for (i, &(acc_id, _)) in init_accs.iter().enumerate() {
let bal_after = state.get_account_by_id(acc_id).balance;
assert_eq!(
balances_before[i],
bal_after,
"INVARIANT VIOLATION [StateIsolationOnFailure]: \
program deployment failure changed balance of account {:?} \
(before={}, after={})",
acc_id,
balances_before[i],
bal_after,
);
let nonce_after = state.get_account_by_id(acc_id).nonce;
assert_eq!(
nonces_before[i],
nonce_after,
"INVARIANT VIOLATION [StateIsolationOnFailure]: \
program deployment failure changed nonce of account {:?}",
acc_id,
);
}
}
Ok(()) => {
// ── Invariant 2: BalanceIsolation ─────────────────────────────────
// On success, no genesis account balance may change — deployment
// only inserts a new program entry, it does not move tokens.
for (i, &(acc_id, _)) in init_accs.iter().enumerate() {
let bal_after = state.get_account_by_id(acc_id).balance;
assert_eq!(
balances_before[i],
bal_after,
"INVARIANT VIOLATION [BalanceIsolation]: \
successful program deployment changed balance of account {:?} \
(before={}, after={}) — deployment must not transfer tokens",
acc_id,
balances_before[i],
bal_after,
);
}
}
}
});