mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-07 03:29:26 +00:00
fix: move to fuzz-driven state generation
This commit is contained in:
parent
50dab4cfb8
commit
18265815e4
@ -6,24 +6,41 @@
|
||||
//!
|
||||
//! `execute_check_on_state` returns the transaction back on success (`Ok(tx)`),
|
||||
//! so we can feed the same struct to the second application without cloning.
|
||||
//!
|
||||
//! The initial state is generated from the fuzz input (rather than a fixed
|
||||
//! testnet genesis) so that nonce-dependent edge cases — e.g. replay prevention
|
||||
//! at nonce 0, nonce `u128::MAX`, or when the sender has zero balance — are
|
||||
//! reachable by the fuzzer.
|
||||
|
||||
use arbitrary::Unstructured;
|
||||
use fuzz_props::generators::arbitrary_transaction;
|
||||
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;
|
||||
use testnet_initial_state::initial_accounts;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
let accs_data = initial_accounts();
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = accs_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();
|
||||
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||
|
||||
let Ok(tx) = arbitrary_transaction(&mut u) else { return; };
|
||||
// Mix correlated transactions (correctly signed, referencing a fuzz account)
|
||||
// with random ones. Correlated transactions have a higher chance of being
|
||||
// accepted on the first application, which is necessary for the replay check
|
||||
// to fire.
|
||||
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 { return; };
|
||||
|
||||
// Stateless gate: skip structurally malformed transactions.
|
||||
let Ok(tx) = tx.transaction_stateless_check() else { return; };
|
||||
|
||||
@ -6,22 +6,37 @@
|
||||
//!
|
||||
//! A diff that modifies an account outside that set would allow a transaction
|
||||
//! to silently corrupt unrelated accounts' balances.
|
||||
//!
|
||||
//! The initial state is generated from the fuzz input (rather than a fixed
|
||||
//! testnet genesis) so that state-dependent diff bugs — those triggered only by
|
||||
//! specific account shapes such as zero balance or `u128::MAX` — are reachable.
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use fuzz_props::arbitrary_types::ArbPublicTransaction;
|
||||
use fuzz_props::generators::arbitrary_fuzz_state;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use nssa::{V03State, ValidatedStateDiff};
|
||||
use testnet_initial_state::initial_accounts;
|
||||
|
||||
fuzz_target!(|wrapped: ArbPublicTransaction| {
|
||||
let pub_tx = wrapped.0;
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
let accs_data = initial_accounts();
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = accs_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();
|
||||
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||
|
||||
// Generate the public transaction from remaining fuzz bytes.
|
||||
let pub_tx = match ArbPublicTransaction::arbitrary(&mut u) {
|
||||
Ok(w) => w.0,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// Collect the set of accounts the transaction claims to touch.
|
||||
let affected = pub_tx.affected_public_account_ids();
|
||||
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
#![no_main]
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use fuzz_props::generators::arbitrary_transaction;
|
||||
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use nssa::V03State;
|
||||
use testnet_initial_state::initial_accounts;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
// Build genesis account list from testnet initial state
|
||||
let accs_data = initial_accounts();
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = accs_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();
|
||||
@ -22,7 +27,16 @@ fuzz_target!(|data: &[u8]| {
|
||||
// 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 {
|
||||
let Ok(tx) = arbitrary_transaction(&mut u) else {
|
||||
// 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;
|
||||
};
|
||||
|
||||
|
||||
@ -18,23 +18,43 @@
|
||||
//! transaction. This catches double-credit and token-inflation bugs that both
|
||||
//! methods could agree on silently (INVARIANT 2a/2b only check consistency
|
||||
//! between the two methods, not correctness of the arithmetic itself).
|
||||
//!
|
||||
//! The initial state is generated from the fuzz input (rather than a fixed
|
||||
//! testnet genesis) so that state-dependent bugs — those that only manifest
|
||||
//! with specific account shapes such as zero balance or `u128::MAX` — are
|
||||
//! reachable by the fuzzer.
|
||||
|
||||
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;
|
||||
use testnet_initial_state::initial_accounts;
|
||||
|
||||
fuzz_target!(|wrapped: ArbNSSATransaction| {
|
||||
let tx = wrapped.0;
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
// Generate a fuzz-driven initial state. The state shape — account IDs,
|
||||
// balances, and the private keys needed to sign transactions against it —
|
||||
// is fully controlled by the fuzzer, exposing state-dependent bugs that
|
||||
// the fixed testnet genesis would never reach.
|
||||
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 the transaction from the remaining fuzz bytes.
|
||||
let tx = match ArbNSSATransaction::arbitrary(&mut u) {
|
||||
Ok(w) => w.0,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// Stateless gate — skip structurally malformed transactions.
|
||||
let Ok(tx) = tx.transaction_stateless_check() else { return; };
|
||||
|
||||
let accs_data = initial_accounts();
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = accs_data
|
||||
.iter()
|
||||
.map(|a| (a.account_id, a.balance))
|
||||
.collect();
|
||||
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||
|
||||
// validate_on_state borrows `tx` and `state` — does NOT mutate state.
|
||||
|
||||
@ -2,10 +2,79 @@ use arbitrary::{Arbitrary, Unstructured};
|
||||
use common::{block::HashableBlockData, transaction::NSSATransaction};
|
||||
use nssa::{AccountId, PrivateKey};
|
||||
|
||||
use crate::arbitrary_types::ArbNSSATransaction;
|
||||
use crate::arbitrary_types::{ArbAccountId, ArbNSSATransaction, ArbPrivateKey};
|
||||
use proptest::prelude::*;
|
||||
use testnet_initial_state::initial_pub_accounts_private_keys;
|
||||
|
||||
// ── Fuzz-driven state generation ─────────────────────────────────────────────
|
||||
|
||||
/// An account with an arbitrary identifier, balance, and private key,
|
||||
/// generated entirely from unstructured fuzzer bytes.
|
||||
///
|
||||
/// Using random account IDs (rather than the fixed `testnet_initial_state` set)
|
||||
/// exposes state-dependent bugs that only manifest with specific account shapes —
|
||||
/// for example: zero balance, [`u128::MAX`] balance, or a nonce at the
|
||||
/// wrap-around boundary. The [`PrivateKey`] field lets downstream generators
|
||||
/// produce correctly-signed transfers referencing accounts present in this state.
|
||||
pub struct FuzzAccount {
|
||||
pub account_id: AccountId,
|
||||
pub balance: u128,
|
||||
pub private_key: PrivateKey,
|
||||
}
|
||||
|
||||
/// Generate 1–8 fuzz-driven accounts with arbitrary IDs, balances, and keys.
|
||||
///
|
||||
/// Call this before generating transactions so the constructed [`nssa::V03State`]
|
||||
/// has a shape controlled by the fuzzer rather than fixed at compile time.
|
||||
pub fn arbitrary_fuzz_state(u: &mut Unstructured<'_>) -> arbitrary::Result<Vec<FuzzAccount>> {
|
||||
let n = ((u8::arbitrary(u)? as usize) % 8) + 1; // 1..=8
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
Ok(FuzzAccount {
|
||||
account_id: ArbAccountId::arbitrary(u)?.0,
|
||||
balance: u128::arbitrary(u)?,
|
||||
private_key: ArbPrivateKey::arbitrary(u)?.0,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate a native-transfer [`NSSATransaction`] between two accounts chosen
|
||||
/// from `accounts`.
|
||||
///
|
||||
/// Because every account in the slice has a known private key, the resulting
|
||||
/// transaction is correctly signed and references account IDs that actually
|
||||
/// exist in the fuzz-generated state — giving the fuzzer a direct path to
|
||||
/// exercise **successful** state transitions rather than only rejection paths.
|
||||
///
|
||||
/// Self-transfers (`from_idx == to_idx`) are allowed since they are a useful
|
||||
/// edge case (balance should remain unchanged).
|
||||
pub fn arb_fuzz_native_transfer(
|
||||
u: &mut Unstructured<'_>,
|
||||
accounts: &[FuzzAccount],
|
||||
) -> arbitrary::Result<NSSATransaction> {
|
||||
if accounts.is_empty() {
|
||||
return Err(arbitrary::Error::IncorrectFormat);
|
||||
}
|
||||
let from_idx = (u8::arbitrary(u)? as usize) % accounts.len();
|
||||
let to_idx = (u8::arbitrary(u)? as usize) % accounts.len();
|
||||
let nonce = u128::arbitrary(u)?;
|
||||
let amount = u128::arbitrary(u)?;
|
||||
|
||||
let from = &accounts[from_idx];
|
||||
let to = &accounts[to_idx];
|
||||
|
||||
Ok(
|
||||
common::test_utils::create_transaction_native_token_transfer(
|
||||
from.account_id,
|
||||
nonce,
|
||||
to.account_id,
|
||||
amount,
|
||||
&from.private_key,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Arbitrary (for libFuzzer targets) ────────────────────────────────────────
|
||||
|
||||
/// A best-effort attempt to create a structurally plausible `NSSATransaction`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user