mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-14 06:59:29 +00:00
fix: remove input-independent targets
- create unit tests in lez repo instead
This commit is contained in:
parent
9cb0d43c40
commit
3c260eeef0
@ -632,6 +632,40 @@ flag stubs out ZK proof generation and replaces it with a fast mock implementati
|
||||
|
||||
---
|
||||
|
||||
## Mutation testing — the two planes
|
||||
|
||||
Mutation testing here runs in two distinct planes, answering two different questions:
|
||||
|
||||
- **Plane A — "does a test catch this mutant?"** Run with a standard `cargo test`
|
||||
oracle against the `lee` crate's own unit tests.
|
||||
- **Plane B — "does the committed fuzz corpus catch this mutant?"** Run with
|
||||
`just mutants-protocol`, which swaps `cargo test` for a fuzz-corpus replay
|
||||
(`cargo fuzz run … -runs=0`) as the oracle.
|
||||
|
||||
A mutant surviving Plane B is **not automatically a corpus gap to fill.** Some
|
||||
mutations are only reachable by a fully-valid executing transaction or by a
|
||||
deliberately-misbehaving program — neither of which a fuzzer can synthesise from
|
||||
random bytes, and both of which are better pinned by deterministic unit tests in
|
||||
the `lee` crate. Encoding such scenarios as input-independent fuzz targets only
|
||||
duplicates those tests and slows every corpus replay.
|
||||
|
||||
The mutants that are **expected** to survive Plane B (and where each is actually
|
||||
covered) are catalogued in [`mutants-not-fuzzable.md`](mutants-not-fuzzable.md).
|
||||
Reconcile new `mutants-protocol` runs against that list: only a surviving mutant
|
||||
**not** on it warrants a new corpus input.
|
||||
|
||||
**No input-independent targets.** A fuzz target whose closure ignores its input
|
||||
(`|_data|`) is a deterministic unit test, not a fuzzer — it belongs in the LEZ
|
||||
crate that owns the code. Three such targets once existed
|
||||
(`fuzz_common_invariants`, `fuzz_genesis_invariants`,
|
||||
`fuzz_system_account_protection`); their invariants were ported to LEZ unit tests
|
||||
and the targets removed. The mutant→test mapping and verification are recorded in
|
||||
[`input-independent-target-coverage.md`](input-independent-target-coverage.md).
|
||||
When adding a target, drive it from `data`; if a check doesn't depend on the
|
||||
input, write it as a unit test in `logos-execution-zone` instead.
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations & Future Work
|
||||
|
||||
| Item | Notes |
|
||||
|
||||
@ -126,18 +126,6 @@ path = "fuzz_targets/fuzz_merkle_tree.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_genesis_invariants"
|
||||
path = "fuzz_targets/fuzz_genesis_invariants.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_common_invariants"
|
||||
path = "fuzz_targets/fuzz_common_invariants.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_transaction_properties"
|
||||
path = "fuzz_targets/fuzz_transaction_properties.rs"
|
||||
@ -161,9 +149,3 @@ name = "fuzz_nullifier_set_roundtrip"
|
||||
path = "fuzz_targets/fuzz_nullifier_set_roundtrip.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_system_account_protection"
|
||||
path = "fuzz_targets/fuzz_system_account_protection.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
@ -1,171 +0,0 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: common-crate and low-level type invariants.
|
||||
//!
|
||||
//! This target is **input-independent**: the fuzz input is always ignored.
|
||||
//! It asserts deterministic invariants about types in `lez/common` and
|
||||
//! low-level `lee` types that are not exercised by higher-level state-transition
|
||||
//! targets.
|
||||
//!
|
||||
//! # Corpus note
|
||||
//!
|
||||
//! A single `\x00` seed file is sufficient — the input bytes are never read.
|
||||
|
||||
use common::{HashType, config::BasicAuth};
|
||||
use nssa::{
|
||||
privacy_preserving_transaction::circuit::Proof,
|
||||
program::Program,
|
||||
program_deployment_transaction::Message as DeployMessage,
|
||||
program_methods::{
|
||||
AUTHENTICATED_TRANSFER_ELF, TOKEN_ELF,
|
||||
},
|
||||
};
|
||||
|
||||
fuzz_props::fuzz_entry!(|_data: &[u8]| {
|
||||
// ── INVARIANT [HashTypeAsRefLength] ────────────────────────────────────────
|
||||
// `HashType::as_ref()` must always return exactly 32 bytes.
|
||||
// Catches mutations that return an empty slice or a slice of the wrong size.
|
||||
let all_ones = HashType([1_u8; 32]);
|
||||
assert_eq!(
|
||||
all_ones.as_ref().len(),
|
||||
32,
|
||||
"INVARIANT VIOLATION [HashTypeAsRefLength]: HashType::as_ref must return 32 bytes",
|
||||
);
|
||||
|
||||
let zero = HashType::default();
|
||||
assert_eq!(
|
||||
zero.as_ref().len(),
|
||||
32,
|
||||
"INVARIANT VIOLATION [HashTypeAsRefLength]: HashType::as_ref on default must return 32 bytes",
|
||||
);
|
||||
|
||||
// ── INVARIANT [HashTypeAsRefBytes] ────────────────────────────────────────
|
||||
// `HashType::as_ref()` must return the exact inner bytes.
|
||||
// Catches mutations that return `vec![0]` or `vec![1]` instead of `&self.0`.
|
||||
let known = [0x42_u8; 32];
|
||||
let hash = HashType(known);
|
||||
assert_eq!(
|
||||
hash.as_ref(),
|
||||
&known,
|
||||
"INVARIANT VIOLATION [HashTypeAsRefBytes]: HashType::as_ref must return the inner [u8;32]",
|
||||
);
|
||||
|
||||
// ── INVARIANT [BasicAuthPasswordPreserved] ───────────────────────────────
|
||||
// Parsing "user:password" must preserve the non-empty password as `Some`.
|
||||
// Catches the mutation that deletes `!` in the `.filter(|p| !p.is_empty())`
|
||||
// predicate, which would flip the logic and accept only empty passwords.
|
||||
let auth: BasicAuth = "user:secret"
|
||||
.parse()
|
||||
.expect("INVARIANT VIOLATION: 'user:secret' must parse as BasicAuth");
|
||||
assert_eq!(
|
||||
auth.password.as_deref(),
|
||||
Some("secret"),
|
||||
"INVARIANT VIOLATION [BasicAuthPasswordPreserved]: \
|
||||
parsing 'user:secret' must give password = Some(\"secret\")",
|
||||
);
|
||||
|
||||
let auth2: BasicAuth = "alice:hunter2"
|
||||
.parse()
|
||||
.expect("INVARIANT VIOLATION: 'alice:hunter2' must parse");
|
||||
assert_eq!(
|
||||
auth2.password.as_deref(),
|
||||
Some("hunter2"),
|
||||
"INVARIANT VIOLATION [BasicAuthPasswordPreserved]: \
|
||||
password must match the part after the colon",
|
||||
);
|
||||
|
||||
// ── INVARIANT [BasicAuthEmptyPasswordIsNone] ─────────────────────────────
|
||||
// Parsing "user:" (empty password) must give `password = None`.
|
||||
// With the `!` deleted, this would become `Some("")` instead of `None`.
|
||||
let auth_empty: BasicAuth = "user:"
|
||||
.parse()
|
||||
.expect("INVARIANT VIOLATION: 'user:' must parse as BasicAuth");
|
||||
assert_eq!(
|
||||
auth_empty.password,
|
||||
None,
|
||||
"INVARIANT VIOLATION [BasicAuthEmptyPasswordIsNone]: \
|
||||
an empty password (trailing colon) must give password = None",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ProgramElfNonEmpty] ───────────────────────────────────────
|
||||
// `Program::elf()` must return a non-empty byte slice.
|
||||
// Catches the mutation that returns `Vec::leak(Vec::new())`.
|
||||
let at_prog = Program::authenticated_transfer_program();
|
||||
assert!(
|
||||
!at_prog.elf().is_empty(),
|
||||
"INVARIANT VIOLATION [ProgramElfNonEmpty]: \
|
||||
Program::authenticated_transfer_program().elf() must not be empty",
|
||||
);
|
||||
|
||||
let token_prog = Program::token();
|
||||
assert!(
|
||||
!token_prog.elf().is_empty(),
|
||||
"INVARIANT VIOLATION [ProgramElfNonEmpty]: \
|
||||
Program::token().elf() must not be empty",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ProgramElfCorrect] ────────────────────────────────────────
|
||||
// `Program::elf()` must return exactly the compile-time bytecode constant.
|
||||
// Catches the mutations that return `vec![0]` or `vec![1]`.
|
||||
assert_eq!(
|
||||
at_prog.elf(),
|
||||
AUTHENTICATED_TRANSFER_ELF,
|
||||
"INVARIANT VIOLATION [ProgramElfCorrect]: \
|
||||
Program::authenticated_transfer_program().elf() must equal AUTHENTICATED_TRANSFER_ELF",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
token_prog.elf(),
|
||||
TOKEN_ELF,
|
||||
"INVARIANT VIOLATION [ProgramElfCorrect]: \
|
||||
Program::token().elf() must equal TOKEN_ELF",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ProofIntoInnerRoundtrip] ──────────────────────────────────
|
||||
// `Proof::from_inner(bytes).into_inner()` must return the original bytes.
|
||||
// Catches the mutations that return `vec![]`, `vec![0]`, or `vec![1]`.
|
||||
let proof_bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF];
|
||||
let proof = Proof::from_inner(proof_bytes.clone());
|
||||
assert_eq!(
|
||||
proof.into_inner(),
|
||||
proof_bytes,
|
||||
"INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \
|
||||
Proof::from_inner(b).into_inner() must return b",
|
||||
);
|
||||
|
||||
// Also test with an empty proof (round-trip must preserve emptiness).
|
||||
let empty_proof = Proof::from_inner(vec![]);
|
||||
assert!(
|
||||
empty_proof.into_inner().is_empty(),
|
||||
"INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \
|
||||
empty Proof::from_inner(vec![]).into_inner() must be empty",
|
||||
);
|
||||
|
||||
// And with a single non-zero byte:
|
||||
let single = Proof::from_inner(vec![0xFF]);
|
||||
assert_eq!(
|
||||
single.into_inner(),
|
||||
vec![0xFF_u8],
|
||||
"INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \
|
||||
Proof from single byte must round-trip correctly",
|
||||
);
|
||||
|
||||
// ── INVARIANT [DeployMessageBytecodeRoundtrip] ────────────────────────────
|
||||
// `Message::new(bytecode).into_bytecode()` must return the original bytecode.
|
||||
// Catches the mutations that return `vec![]`, `vec![0]`, or `vec![1]`.
|
||||
let bytecode = vec![0x7F_u8, 0x45, 0x4C, 0x46]; // ELF magic
|
||||
let msg = DeployMessage::new(bytecode.clone());
|
||||
assert_eq!(
|
||||
msg.into_bytecode(),
|
||||
bytecode,
|
||||
"INVARIANT VIOLATION [DeployMessageBytecodeRoundtrip]: \
|
||||
Message::new(b).into_bytecode() must return b",
|
||||
);
|
||||
|
||||
// Empty bytecode round-trip:
|
||||
let empty_msg = DeployMessage::new(vec![]);
|
||||
assert!(
|
||||
empty_msg.into_bytecode().is_empty(),
|
||||
"INVARIANT VIOLATION [DeployMessageBytecodeRoundtrip]: \
|
||||
empty bytecode must round-trip as empty",
|
||||
);
|
||||
});
|
||||
@ -1,137 +0,0 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: genesis-state and system-account invariants.
|
||||
//!
|
||||
//! This target is **input-independent**: the fuzz input is always ignored.
|
||||
//! It asserts deterministic invariants about the genesis state produced by
|
||||
//! `V03State::new_with_genesis_accounts`, `system_faucet_account_id`,
|
||||
//! `system_bridge_account_id`, and `V03State::add_pinata_token_program`.
|
||||
//!
|
||||
//! # Covered mutations (from `lee/state_machine/src/state.rs`)
|
||||
//!
|
||||
//! | Line | Mutation | Assertion that catches it |
|
||||
//! |------|--------------------------------------------------------|-----------------------------------------------------|
|
||||
//! | 312 | `commitment_set_digest → Default::default()` | `[CommitmentSetDigestNonDefault]` |
|
||||
//! | 368 | delete `program_owner` from `add_pinata_token_program` | `[PinataTokenProgramOwner]` |
|
||||
//! | 370 | delete `data` from `add_pinata_token_program` | `[PinataTokenData]` |
|
||||
//! | 385 | `system_faucet_account → Default::default()` | `[FaucetBalance]` + `[FaucetProgramOwner]` |
|
||||
//! | 386 | delete `program_owner` from `system_faucet_account` | `[FaucetProgramOwner]` |
|
||||
//! | 387 | delete `balance` from `system_faucet_account` | `[FaucetBalance]` |
|
||||
//! | 393 | `system_bridge_account → Default::default()` | `[BridgeProgramOwner]` |
|
||||
//! | 394 | delete `program_owner` from `system_bridge_account` | `[BridgeProgramOwner]` |
|
||||
//! | 406 | `system_bridge_account_id → Default::default()` | `[BridgeIdNonDefault]` + `[SystemAccountIdDistinct]` |
|
||||
//!
|
||||
//! # Corpus note
|
||||
//!
|
||||
//! A single `\x00` seed file is sufficient — the input bytes are never read.
|
||||
//! The seed is required by `cargo fuzz run -runs=0` so that the replay phase
|
||||
//! has at least one execution to check against.
|
||||
|
||||
use nssa::{Account, AccountId, V03State, system_bridge_account_id, system_faucet_account_id};
|
||||
|
||||
fuzz_props::fuzz_entry!(|_data: &[u8]| {
|
||||
let default_account = Account::default();
|
||||
|
||||
// ── INVARIANT [BridgeIdNonDefault] ────────────────────────────────────────
|
||||
// `system_bridge_account_id()` must return a non-default `AccountId`.
|
||||
// Catches the mutation at state.rs:406 that replaces the function body with
|
||||
// `Default::default()`.
|
||||
let bridge_id = system_bridge_account_id();
|
||||
assert_ne!(
|
||||
bridge_id,
|
||||
AccountId::default(),
|
||||
"INVARIANT VIOLATION [BridgeIdNonDefault]: \
|
||||
system_bridge_account_id() must not return AccountId::default()",
|
||||
);
|
||||
|
||||
// The two system account IDs must also be distinct so that they occupy
|
||||
// separate entries in the public-state map.
|
||||
let faucet_id = system_faucet_account_id();
|
||||
assert_ne!(
|
||||
faucet_id,
|
||||
bridge_id,
|
||||
"INVARIANT VIOLATION [SystemAccountIdDistinct]: \
|
||||
system_faucet_account_id() and system_bridge_account_id() must differ",
|
||||
);
|
||||
|
||||
// Build the genesis state with no extra accounts.
|
||||
let state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
|
||||
// ── INVARIANT [FaucetBalance] ─────────────────────────────────────────────
|
||||
// The system faucet account must hold `u128::MAX` tokens.
|
||||
// Catches state.rs:385 (whole account → Default) and
|
||||
// state.rs:387 (delete `balance` field from struct literal).
|
||||
let faucet = state.get_account_by_id(faucet_id);
|
||||
assert_eq!(
|
||||
faucet.balance,
|
||||
u128::MAX,
|
||||
"INVARIANT VIOLATION [FaucetBalance]: \
|
||||
system_faucet_account must have balance == u128::MAX, got {}",
|
||||
faucet.balance,
|
||||
);
|
||||
|
||||
// ── INVARIANT [FaucetProgramOwner] ────────────────────────────────────────
|
||||
// The system faucet account must have a non-default `program_owner`.
|
||||
// Catches state.rs:385 (whole account → Default) and
|
||||
// state.rs:386 (delete `program_owner` field from struct literal).
|
||||
assert_ne!(
|
||||
faucet.program_owner,
|
||||
default_account.program_owner,
|
||||
"INVARIANT VIOLATION [FaucetProgramOwner]: \
|
||||
system_faucet_account must have a non-default program_owner",
|
||||
);
|
||||
|
||||
// ── INVARIANT [BridgeProgramOwner] ───────────────────────────────────────
|
||||
// The system bridge account must have a non-default `program_owner`.
|
||||
// Catches state.rs:393 (whole account → Default) and
|
||||
// state.rs:394 (delete `program_owner` field from struct literal).
|
||||
let bridge = state.get_account_by_id(bridge_id);
|
||||
assert_ne!(
|
||||
bridge.program_owner,
|
||||
default_account.program_owner,
|
||||
"INVARIANT VIOLATION [BridgeProgramOwner]: \
|
||||
system_bridge_account must have a non-default program_owner",
|
||||
);
|
||||
|
||||
// ── INVARIANT [CommitmentSetDigestNonDefault] ─────────────────────────────
|
||||
// A freshly created empty state has an all-zero Merkle root, which equals
|
||||
// `CommitmentSetDigest::default()`. The genesis state inserts
|
||||
// `DUMMY_COMMITMENT` via SHA-256, producing a strictly different root.
|
||||
// Catches state.rs:312 that replaces `commitment_set_digest()` with
|
||||
// `Default::default()`.
|
||||
let empty_digest = V03State::new().commitment_set_digest();
|
||||
let genesis_digest = state.commitment_set_digest();
|
||||
assert_ne!(
|
||||
genesis_digest,
|
||||
empty_digest,
|
||||
"INVARIANT VIOLATION [CommitmentSetDigestNonDefault]: \
|
||||
commitment_set_digest of genesis state must differ from the empty state's \
|
||||
all-zero root",
|
||||
);
|
||||
|
||||
// ── INVARIANT [PinataTokenProgramOwner] ──────────────────────────────────
|
||||
// An account created by `add_pinata_token_program` must have a non-default
|
||||
// `program_owner` field.
|
||||
// Catches state.rs:368 (delete `program_owner` from the struct literal).
|
||||
//
|
||||
// ── INVARIANT [PinataTokenData] ──────────────────────────────────────────
|
||||
// An account created by `add_pinata_token_program` must have non-default
|
||||
// `data` (specifically `vec![3; 33]` encoded as `Data`).
|
||||
// Catches state.rs:370 (delete `data` from the struct literal).
|
||||
let pt_id = AccountId::new([0xABu8; 32]);
|
||||
let mut pinata_state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
pinata_state.add_pinata_token_program(pt_id);
|
||||
let pt = pinata_state.get_account_by_id(pt_id);
|
||||
|
||||
assert_ne!(
|
||||
pt.program_owner,
|
||||
default_account.program_owner,
|
||||
"INVARIANT VIOLATION [PinataTokenProgramOwner]: \
|
||||
add_pinata_token_program must set a non-default program_owner on the account",
|
||||
);
|
||||
assert_ne!(
|
||||
pt.data,
|
||||
default_account.data,
|
||||
"INVARIANT VIOLATION [PinataTokenData]: \
|
||||
add_pinata_token_program must set non-default data on the account",
|
||||
);
|
||||
});
|
||||
@ -1,165 +0,0 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: system-account modification protection.
|
||||
//!
|
||||
//! `LeeTransaction::validate_on_state` must reject any transaction that modifies
|
||||
//! a system account (faucet, bridge, or clock accounts). This is enforced by
|
||||
//! `validate_doesnt_modify_account` which inspects `ValidatedStateDiff::public_diff()`.
|
||||
//!
|
||||
//! # Corpus note
|
||||
//!
|
||||
//! This target is **input-independent**. A single `\x00` seed is sufficient.
|
||||
//!
|
||||
//! **Performance note**: the `[SystemAccountModificationRejected]` invariant
|
||||
//! executes a RISC0 program (a native transfer). This is inherently slow
|
||||
//! (~seconds). Only one corpus file is needed, so the corpus-regression oracle
|
||||
//! costs one program execution per mutant under test.
|
||||
|
||||
use common::transaction::LeeTransaction;
|
||||
use nssa::{
|
||||
AccountId, PrivateKey, PublicKey, V03State, ValidatedStateDiff,
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, system_bridge_account_id, system_faucet_account_id,
|
||||
};
|
||||
|
||||
fuzz_props::fuzz_entry!(|_data: &[u8]| {
|
||||
// ── INVARIANT [SystemAccountIdsDistinct] ──────────────────────────────────
|
||||
let faucet_id = system_faucet_account_id();
|
||||
let bridge_id = system_bridge_account_id();
|
||||
|
||||
assert_ne!(
|
||||
faucet_id,
|
||||
AccountId::default(),
|
||||
"INVARIANT VIOLATION [SystemAccountIdsDistinct]: faucet account ID must be non-default",
|
||||
);
|
||||
assert_ne!(
|
||||
bridge_id,
|
||||
AccountId::default(),
|
||||
"INVARIANT VIOLATION [SystemAccountIdsDistinct]: bridge account ID must be non-default",
|
||||
);
|
||||
assert_ne!(
|
||||
faucet_id,
|
||||
bridge_id,
|
||||
"INVARIANT VIOLATION [SystemAccountIdsDistinct]: faucet and bridge must be distinct",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ClockInvocationRejected] ──────────────────────────────────
|
||||
// A native transfer that CREDITS a clock system account modifies exactly one
|
||||
// system account — the clock account — and that account is *changed* (its
|
||||
// balance increases from 0). No other system account appears in the diff, so
|
||||
// this isolates the `validate_doesnt_modify_account` rejection cleanly.
|
||||
//
|
||||
// Why not a clock invocation? The clock program writes all three clock
|
||||
// accounts, but the 01/10/50 clocks tick at different rates, so its diff
|
||||
// contains BOTH changed and unchanged system accounts. The `!=`→`==`
|
||||
// mutation then still rejects (citing an unchanged account), so a clock
|
||||
// invocation cannot distinguish the mutant. Crediting a single clock account
|
||||
// gives a single, changed system account, which the mutant must accept.
|
||||
//
|
||||
// Why not credit the faucet? The faucet holds u128::MAX, so any credit
|
||||
// overflows and the program execution fails before the protection check is
|
||||
// reached. A clock account starts at balance 0, so a small credit succeeds.
|
||||
//
|
||||
// With mutation `!=` → `==` at transaction.rs:182:
|
||||
// The clock account is changed (post != pre), so the mutated `post == pre`
|
||||
// check is false → no error → validate_on_state returns Ok → our assert fires.
|
||||
//
|
||||
// With mutation `public_diff → HashMap::new()` at validated_state_diff.rs:479:
|
||||
// validate_doesnt_modify_account sees an empty map → can never find the
|
||||
// clock account → returns Ok for every transaction → our assert fires.
|
||||
{
|
||||
let sender_key = PrivateKey::try_new([5_u8; 32]).expect("known-good key");
|
||||
let sender_pub = PublicKey::new_from_private_key(&sender_key);
|
||||
let sender_id = AccountId::from(&sender_pub);
|
||||
|
||||
// Fund the sender; clock accounts already exist in genesis (balance 0).
|
||||
let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000_u128)], vec![], 0);
|
||||
|
||||
// Transfer tokens TO a clock account — credits (changes) that system account.
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
sender_id,
|
||||
0, // nonce
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
100, // amount credited to the clock account
|
||||
&sender_key,
|
||||
);
|
||||
|
||||
let result = tx.validate_on_state(&state, 1, 0);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"INVARIANT VIOLATION [SystemAccountModificationRejected]: \
|
||||
validate_on_state must reject a transfer that credits a clock system \
|
||||
account. If this fires, either validate_doesnt_modify_account has a logic \
|
||||
inversion (!=→==) or public_diff() returns an empty map",
|
||||
);
|
||||
}
|
||||
|
||||
// ── INVARIANT [PublicDiffNonEmptyOnSuccess] ────────────────────────────────
|
||||
// For a valid public transaction with signers, the signer accounts must appear
|
||||
// in public_diff after successful validation (nonces are updated in the diff).
|
||||
//
|
||||
// With mutation `public_diff → HashMap::new()`:
|
||||
// The map is empty → `contains_key(&signer)` returns false → assert fires.
|
||||
//
|
||||
// Uses `common::test_utils::create_transaction_native_token_transfer` to
|
||||
// construct a semantically valid transaction (correct instruction type).
|
||||
{
|
||||
let key = PrivateKey::try_new([7_u8; 32]).expect("known-good key");
|
||||
let pubkey = PublicKey::new_from_private_key(&key);
|
||||
let addr = AccountId::from(&pubkey);
|
||||
|
||||
let key2 = PrivateKey::try_new([8_u8; 32]).expect("known-good key");
|
||||
let pubkey2 = PublicKey::new_from_private_key(&key2);
|
||||
let addr2 = AccountId::from(&pubkey2);
|
||||
|
||||
let state = V03State::new_with_genesis_accounts(
|
||||
&[(addr, 10_000_u128), (addr2, 10_000_u128)],
|
||||
vec![],
|
||||
0,
|
||||
);
|
||||
|
||||
// Use the test utility to build a valid native token transfer.
|
||||
// This uses the correct authenticated_transfer_core::Instruction::Transfer,
|
||||
// which the program can actually execute without panicking.
|
||||
let lee_tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
addr,
|
||||
0, // nonce = 0 (matches initial state nonce)
|
||||
addr2,
|
||||
100, // amount
|
||||
&key,
|
||||
);
|
||||
|
||||
if let LeeTransaction::Public(pub_tx) = &lee_tx {
|
||||
if let Ok(diff) = ValidatedStateDiff::from_public_transaction(&pub_tx, &state, 1, 0) {
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
// The signer/sender (addr) must be in the diff: a native transfer
|
||||
// debits its balance, so it MUST appear in public_diff.
|
||||
// If public_diff() returns an empty HashMap, this assert fires.
|
||||
//
|
||||
// Note: nonce increments are applied separately during
|
||||
// `apply_state_diff` via `signer_account_ids` and are NOT recorded
|
||||
// in `public_diff`, so we do not assert on the nonce field here.
|
||||
assert!(
|
||||
public_diff.contains_key(&addr),
|
||||
"INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \
|
||||
public_diff must contain the sender {:?} (its balance is debited) \
|
||||
after a successful native transfer \
|
||||
(mutation public_diff→HashMap::new() detected)",
|
||||
addr,
|
||||
);
|
||||
|
||||
// The diff must reflect the balance debit on the sender — the
|
||||
// balance recorded in the diff must differ from the pre-state.
|
||||
let pre_balance = state.get_account_by_id(addr).balance;
|
||||
let post_balance = public_diff[&addr].balance;
|
||||
assert_ne!(
|
||||
post_balance,
|
||||
pre_balance,
|
||||
"INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \
|
||||
sender balance in the diff must differ from pre-state after a transfer \
|
||||
(pre={pre_balance}, post={post_balance})",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -37,13 +37,10 @@ targets=(
|
||||
fuzz_multi_block_state_sequence
|
||||
fuzz_sequencer_vs_replayer
|
||||
fuzz_merkle_tree
|
||||
fuzz_genesis_invariants
|
||||
fuzz_common_invariants
|
||||
fuzz_transaction_properties
|
||||
fuzz_privacy_preserving_witness
|
||||
fuzz_encoding_privacy_preserving
|
||||
fuzz_nullifier_set_roundtrip
|
||||
fuzz_system_account_protection
|
||||
)
|
||||
|
||||
# cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user