mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-14 15:09:32 +00:00
272 lines
12 KiB
Rust
272 lines
12 KiB
Rust
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
|
//! Fuzz target: transaction property invariants.
|
|
//!
|
|
//! Tests that key accessor methods on `LeeTransaction`, `PublicTransaction`, and
|
|
//! `ValidatedStateDiff` return correct, non-stub values.
|
|
|
|
use arbitrary::{Arbitrary, Unstructured};
|
|
use common::transaction::LeeTransaction;
|
|
use fuzz_props::arbitrary_types::ArbPrivateKey;
|
|
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state};
|
|
use nssa::{
|
|
AccountId, PrivateKey, PublicKey, ValidatedStateDiff, V03State,
|
|
public_transaction::{Message, WitnessSet},
|
|
PublicTransaction,
|
|
program::Program,
|
|
};
|
|
use nssa_core::account::Nonce;
|
|
|
|
fuzz_props::fuzz_entry!(|data: &[u8]| {
|
|
let mut u = Unstructured::new(data);
|
|
|
|
// ── Part 1: Known-good witness set / transaction using fixed keys ──────────
|
|
// Uses deterministic keys so we always have at least one valid transaction.
|
|
// This ensures hash, signer_account_ids, into_raw_parts, and affected_accounts
|
|
// are always tested, even when the fuzzer input is insufficient for arb generators.
|
|
{
|
|
let key1 = PrivateKey::try_new([1_u8; 32]).expect("known-good key");
|
|
let key2 = PrivateKey::try_new([2_u8; 32]).expect("known-good key");
|
|
let pub1 = PublicKey::new_from_private_key(&key1);
|
|
let pub2 = PublicKey::new_from_private_key(&key2);
|
|
let addr1 = AccountId::from(&pub1);
|
|
let addr2 = AccountId::from(&pub2);
|
|
|
|
let nonces = vec![Nonce::from(0_u128), Nonce::from(0_u128)];
|
|
let message = Message::try_new(
|
|
Program::authenticated_transfer_program().id(),
|
|
vec![addr1, addr2],
|
|
nonces,
|
|
1337_u64,
|
|
)
|
|
.expect("known-good message");
|
|
|
|
let ws = WitnessSet::for_message(&message, &[&key1, &key2]);
|
|
let pub_tx = PublicTransaction::new(message, ws);
|
|
|
|
// ── INVARIANT [SignerIdsNonEmpty] ─────────────────────────────────────
|
|
// A transaction signed by 2 keys must expose 2 signer (key, sig) pairs.
|
|
// `signer_account_ids` is pub(crate); we verify via the public witness_set API.
|
|
let ws_pairs = pub_tx.witness_set().signatures_and_public_keys();
|
|
assert_eq!(
|
|
ws_pairs.len(),
|
|
2,
|
|
"INVARIANT VIOLATION [SignerIdsNonEmpty]: \
|
|
witness_set signatures_and_public_keys must have 2 entries",
|
|
);
|
|
|
|
// ── INVARIANT [IntoRawPartsCount] ─────────────────────────────────────
|
|
// `into_raw_parts` must return the same number of pairs as the witness set.
|
|
// Catches the mutation that returns `vec![]`.
|
|
let ws2 = WitnessSet::for_message(pub_tx.message(), &[&key1, &key2]);
|
|
let parts = ws2.into_raw_parts();
|
|
assert_eq!(
|
|
parts.len(),
|
|
2,
|
|
"INVARIANT VIOLATION [IntoRawPartsCount]: \
|
|
WitnessSet::into_raw_parts must return 2 pairs for a 2-key witness set",
|
|
);
|
|
|
|
// ── INVARIANT [AffectedAccountsContainSigners] ───────────────────────
|
|
// `affected_public_account_ids` must include the signer accounts.
|
|
// Catches the mutation that returns `vec![]` or `vec![Default::default()]`.
|
|
let affected = pub_tx.affected_public_account_ids();
|
|
assert!(
|
|
!affected.is_empty(),
|
|
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
|
affected_public_account_ids must be non-empty for a 2-signer tx",
|
|
);
|
|
assert!(
|
|
affected.contains(&addr1),
|
|
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
|
affected_public_account_ids must include addr1 (signer)",
|
|
);
|
|
assert!(
|
|
affected.contains(&addr2),
|
|
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
|
affected_public_account_ids must include addr2 (signer)",
|
|
);
|
|
|
|
// ── INVARIANT [HashNonDefault] ────────────────────────────────────────
|
|
// The transaction hash must not be the all-zero default.
|
|
// Catches the mutation that returns `Default::default()`.
|
|
let lee_tx = LeeTransaction::Public(pub_tx);
|
|
let hash = lee_tx.hash();
|
|
assert_ne!(
|
|
hash.0,
|
|
[0_u8; 32],
|
|
"INVARIANT VIOLATION [HashNonDefault]: \
|
|
LeeTransaction::hash must not return all-zero bytes",
|
|
);
|
|
|
|
// Also verify it's deterministic (same tx → same hash):
|
|
let hash2 = lee_tx.hash();
|
|
assert_eq!(
|
|
hash,
|
|
hash2,
|
|
"INVARIANT VIOLATION [HashDeterministic]: \
|
|
LeeTransaction::hash must be deterministic",
|
|
);
|
|
|
|
// LeeTransaction::affected_public_account_ids must also be non-empty:
|
|
let lee_affected = lee_tx.affected_public_account_ids();
|
|
assert!(
|
|
lee_affected.contains(&addr1),
|
|
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
|
LeeTransaction::affected_public_account_ids must include addr1",
|
|
);
|
|
}
|
|
|
|
// ── INVARIANT [SignerOnlyAccountInAffected] ───────────────────────────────
|
|
// Build a transaction signed by a key whose AccountId is NOT among
|
|
// `message.account_ids`. Then `affected_public_account_ids` can only contain
|
|
// the signer's AccountId via `signer_account_ids()` — it is absent from the
|
|
// message's account list. This directly catches the `signer_account_ids`
|
|
// mutations (`→ vec![]` / `→ vec![Default::default()]`) on PublicTransaction,
|
|
// which the earlier checks miss because there the signer also appears in
|
|
// `message.account_ids`.
|
|
{
|
|
// Signer key — its AccountId must NOT appear in the message account list.
|
|
let signer_key = PrivateKey::try_new([9_u8; 32]).expect("known-good key");
|
|
let signer_pub = PublicKey::new_from_private_key(&signer_key);
|
|
let signer_addr = AccountId::from(&signer_pub);
|
|
|
|
// Two unrelated account IDs for the message (deterministic, not derived
|
|
// from the signer key).
|
|
let other1 = AccountId::new([0xA1_u8; 32]);
|
|
let other2 = AccountId::new([0xA2_u8; 32]);
|
|
|
|
// Guard: ensure the signer is genuinely not one of the message accounts.
|
|
if signer_addr != other1 && signer_addr != other2 {
|
|
let nonces = vec![Nonce::from(0_u128)];
|
|
if let Ok(msg) = Message::try_new(
|
|
Program::authenticated_transfer_program().id(),
|
|
vec![other1, other2],
|
|
nonces,
|
|
7_u64,
|
|
) {
|
|
let ws = WitnessSet::for_message(&msg, &[&signer_key]);
|
|
let pt = PublicTransaction::new(msg, ws);
|
|
|
|
let affected = pt.affected_public_account_ids();
|
|
assert!(
|
|
affected.contains(&signer_addr),
|
|
"INVARIANT VIOLATION [SignerOnlyAccountInAffected]: \
|
|
affected_public_account_ids must include the signer {:?} even when it \
|
|
is absent from message.account_ids — signer_account_ids() must not \
|
|
return an empty (or defaulted) vec",
|
|
signer_addr,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Part 2: Fuzz-driven state + valid native transfer ─────────────────────
|
|
// Generates a random state and a correctly-signed transfer. When the transfer
|
|
// succeeds, verifies that `public_diff` is non-empty and contains the
|
|
// expected account changes.
|
|
{
|
|
let fuzz_accs = match arbitrary_fuzz_state(&mut u) {
|
|
Ok(accs) => accs,
|
|
Err(_) => return,
|
|
};
|
|
let init_accs: Vec<(AccountId, u128)> = fuzz_accs
|
|
.iter()
|
|
.map(|a| (a.account_id, a.balance))
|
|
.collect();
|
|
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
|
|
|
let Ok(tx) = arb_fuzz_native_transfer(&mut u, &fuzz_accs) else {
|
|
return;
|
|
};
|
|
let Ok(checked) = tx.transaction_stateless_check() else {
|
|
return;
|
|
};
|
|
|
|
let pub_tx = match &checked {
|
|
LeeTransaction::Public(pt) => pt,
|
|
_ => return,
|
|
};
|
|
|
|
// For a public transaction with signers, affected_public_account_ids must
|
|
// include all signer account IDs. Derive signers from the public witness API.
|
|
let signers: Vec<AccountId> = pub_tx
|
|
.witness_set()
|
|
.signatures_and_public_keys()
|
|
.iter()
|
|
.map(|(_, pk)| AccountId::from(pk))
|
|
.collect();
|
|
let affected = pub_tx.affected_public_account_ids();
|
|
for signer in &signers {
|
|
assert!(
|
|
affected.contains(signer),
|
|
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
|
affected_public_account_ids must include signer {:?}",
|
|
signer,
|
|
);
|
|
}
|
|
|
|
// When from_public_transaction succeeds, public_diff must be non-empty
|
|
// (at least the signer nonces are updated).
|
|
// Catches the mutation that returns `HashMap::new()`.
|
|
if let Ok(diff) = ValidatedStateDiff::from_public_transaction(pub_tx, &state, 1, 0) {
|
|
let public_diff = diff.public_diff();
|
|
|
|
// The diff must contain at least the signer accounts (nonce updates):
|
|
for signer in &signers {
|
|
// Signers appear in diff because their nonces are updated.
|
|
// If public_diff() returns empty HashMap, this assert fires.
|
|
assert!(
|
|
public_diff.contains_key(signer),
|
|
"INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \
|
|
public_diff must contain signer account {:?} after successful validation \
|
|
(nonce must have been updated)",
|
|
signer,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Part 3: Fuzz-driven arbitrary keys for additional coverage ─────────────
|
|
{
|
|
if let Ok(key_wrap) = ArbPrivateKey::arbitrary(&mut u) {
|
|
let key = key_wrap.0;
|
|
let pubkey = PublicKey::new_from_private_key(&key);
|
|
let addr = AccountId::from(&pubkey);
|
|
|
|
let nonces = vec![Nonce::from(0_u128)];
|
|
if let Ok(msg) = Message::try_new(
|
|
Program::authenticated_transfer_program().id(),
|
|
vec![addr],
|
|
nonces,
|
|
42_u64,
|
|
) {
|
|
let ws = WitnessSet::for_message(&msg, &[&key]);
|
|
let pt = PublicTransaction::new(msg, ws);
|
|
|
|
// Single-signer checks via witness_set (signer_account_ids is pub(crate)):
|
|
let ws_pairs2 = pt.witness_set().signatures_and_public_keys();
|
|
assert_eq!(
|
|
ws_pairs2.len(),
|
|
1,
|
|
"INVARIANT VIOLATION [SignerIdsNonEmpty]: 1-key witness set must have 1 pair",
|
|
);
|
|
let derived_addr = AccountId::from(&ws_pairs2[0].1);
|
|
assert_eq!(
|
|
derived_addr,
|
|
addr,
|
|
"INVARIANT VIOLATION [SignerIdsDerivedFromKeys]: \
|
|
derived signer address must match expected addr",
|
|
);
|
|
|
|
let ws2 = WitnessSet::for_message(pt.message(), &[&key]);
|
|
let parts = ws2.into_raw_parts();
|
|
assert_eq!(
|
|
parts.len(),
|
|
1,
|
|
"INVARIANT VIOLATION [IntoRawPartsCount]: 1-signer witness set → 1 raw part",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|