lez-fuzzing/README.md
2026-07-02 14:08:00 +02:00

10 KiB
Raw Blame History

Lez-fuzzing

Coverage-guided fuzzing & adversarial testing infrastructure for the Logos Execution Zone (LEZ) protocol.

Rust Fuzzing Mutation testing License


📂 Repository Layout

lez-fuzzing/
├── Cargo.toml              # Workspace root (members: fuzz_props)
├── Justfile                # Turn-key entry-points
├── rust-toolchain.toml     # Pins Rust nightly (required by cargo-fuzz)
├── .gitignore
├── fuzz_props/             # Shared invariant framework + input generators
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs
│       ├── arbitrary_types.rs  # Arbitrary impl wrappers for LEZ types (libFuzzer)
│       ├── invariants.rs       # ProtocolInvariant trait + concrete invariants
│       └── generators.rs       # Arbitrary / proptest strategies
├── fuzz/                   # cargo-fuzz crate (own [workspace] sentinel)
│   ├── Cargo.toml
│   ├── fuzz_targets/       # 22 targets total — see table below
│   │   ├── _template.rs    # Template for `just new-target`
│   │   └── fuzz_*.rs
│   └── corpus/             # Curated seed inputs (one dir per target)
├── .github/
│   └── workflows/
│       ├── fuzz.yml        # CI: smoke-fuzz · regression · proptest · perf (libFuzzer)
│       ├── fuzz-afl.yml    # CI: AFL++ lane
│       ├── mutants.yml     # CI: mutation testing (cargo-mutants)
│       └── lint.yml        # CI: fmt + clippy
├── scripts/
│   └── add_fuzz_target.py  # Automates new-target scaffolding (called by just new-target)
└── docs/
    ├── fuzzing.md              # Full developer guide
    └── mutants-not-fuzzable.md # Policy + mutant→test mapping

The LEZ codebase is consumed as a sibling directory — clone logos-execution-zone next to this repository so the ../ path deps resolve:

parent/
├── lez-fuzzing/            ← this repo
└── logos-execution-zone/   ← LEZ codebase (path deps resolve via ../)

🚀 Quick Start

1. Prerequisites

rustup install nightly
rustup component add llvm-tools-preview --toolchain nightly
cargo install cargo-fuzz
cargo install just            # optional but recommended

2. Setup

# Clone both repositories side by side
git clone <LEZ_REPO_URL> logos-execution-zone
git clone <LEZ_FUZZING_REPO_URL> lez-fuzzing
cd lez-fuzzing

3. Run the fuzz targets

# All targets for 30 s each (RISC0_DEV_MODE=1 is set automatically)
just fuzz

# Specific duration
just fuzz 120

# Single target
RISC0_DEV_MODE=1 cargo fuzz run fuzz_state_transition -- -max_total_time=120

# Corpus regression (replay saved corpus, no mutations)
just fuzz-regression

# Property-based tests only (no libFuzzer)
just fuzz-props

Important

ZK-proof cost: RISC0_DEV_MODE=1 is exported at the top of the Justfile and must be set in every fuzz run to stub out ZK proof generation. Without it, each execution takes seconds instead of microseconds.


🎯 Fuzz Targets

# Target Protocol layer
1 fuzz_transaction_decoding Borsh decoding of all tx/block types (LeeTransaction, Block, HashableBlockData) with roundtrip re-encoding
2 fuzz_stateless_verification transaction_stateless_check() no-panic + idempotency
3 fuzz_state_transition V03State transition: StateIsolationOnFailure + BalanceConservation + ReplayRejection invariants across up to 8 txs with fuzz-driven state
4 fuzz_block_verification Block hash integrity: HashRoundTrip · HashPreimage completeness (block_id/prev_hash/timestamp) · TxOrderCommitment
5 fuzz_encoding_roundtrip Borsh encode→decode→encode round-trip identity + canonical encoding for PublicTransaction and ProgramDeploymentTransaction
6 fuzz_signature_verification Signature correctness (sign→verify), no-panic on random bytes, cross-key soundness
7 fuzz_replay_prevention Transaction nonce replay rejection with fuzz-driven initial state
8 fuzz_state_diff_computation ValidatedStateDiff forward containment + reverse completeness (bidirectional isolation check)
9 fuzz_validate_execute_consistency validate_on_state / execute_check_on_state agreement + diff accuracy + BalanceConservation
10 fuzz_state_serialization V03State Borsh decode no-panic + StateSerializationRoundtrip idempotency + NullifierDeduplication (NullifierSet hand-written impl)
11 fuzz_witness_set_verification WitnessSet::is_valid_for no-panic + CorrectVerification (sign→verify) + MessageIsolation (witness set for msg A rejected on msg B)
12 fuzz_program_deployment_lifecycle V03State::transition_from_program_deployment_transaction no-panic + BalanceIsolation (deployment must not move tokens) + StateIsolationOnFailure
13 fuzz_apply_state_diff_split_path SplitPathEquivalence: validate_on_state + apply_state_diff == execute_check_on_state for all known accounts (balance, nonce, data, program_owner); NonceIncrementCorrectness
14 fuzz_multi_block_state_sequence LongRangeBalanceConservation across up to 16 blocks + FailedTxNonceStability (nonce must not change on rejection) + PerBlockReplayRejection
15 fuzz_sequencer_vs_replayer Differential: sequencer path (validate_on_stateapply_state_diff) vs replayer path (execute_check_on_state) — SequencerReplayerEquivalence + ReplayerAcceptsAllSequencerTxs + ClockConsistency
16 fuzz_merkle_tree Commitment Merkle tree via the commitment set: ProofSome · ProofValid (leaf + auth path recomputes the root) · NonMembershipNone · IndicesSequential
17 fuzz_transaction_properties Transaction property invariants: HashDeterministic/HashNonDefault, SignerIds derived from witness keys & non-empty, AffectedAccountsContainSigners, PublicDiffNonEmptyOnSuccess
18 fuzz_privacy_preserving_witness privacy_preserving_transaction::WitnessSet: CorrectVerification (witness for msg A passes signatures_are_valid_for(A)) + MessageIsolation + SignerIdsMatchWitnessKeys
19 fuzz_encoding_privacy_preserving Privacy-preserving encoding: MessageEncodingRoundtrip + TxEncodingDeterministic/NonEmpty
20 fuzz_nullifier_set_roundtrip NullifierSet Borsh serialisation: NullifierSetRoundtrip (decode→encode identity for the hand-written impl)
21 fuzz_privacy_preserving_state_transition Path B — NSSATransaction::PrivacyPreserving through execute_check_on_state with a dev-mode passing proof: reaches commitment/nullifier checks 56 + apply_state_diff. Asserts no-panic, StateIsolationOnFailure, PrivateStateIsolationOnFailure, CommitmentInsertion, NonceIncrementCorrectness, PostStateApplied, ReplayRejection (balance conservation intentionally not asserted — the fake proof bypasses the circuit guarantee)
22 fuzz_transaction_ordering_independence Transaction ordering-independence on the shielded path: builds a nullifier-conflicting pair (two distinct privacy-preserving txs declaring the same nullifier) and applies it in both orders on independent clones of a seeded state, at an identical (block_id, timestamp). Asserts NoDoubleSpend (neither ordering accepts both — the shared nullifier is spendable at most once) and OrderIndependentAcceptance (the count of accepted txs is the same in both orderings). The nullifier check is enforced by the state machine, not the circuit, so the dev-mode fake proof does not mask it. Requires RISC0_DEV_MODE=1

Each target lives at fuzz/fuzz_targets/<name>.rs.


🧬 Corpus Management

# Minimise all corpora (removes dominated inputs, keeps coverage-equivalent set)
just corpus-cmin

# Minimise a single target's corpus
just corpus-cmin-target fuzz_state_transition

💥 Crash / Failure Workflow

# 1. Minimise a crash artifact
just fuzz-tmin fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-abc123

# 2. Print the bytes as a Rust literal (for a regression #[test])
cargo fuzz fmt fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-abc123

# 3. Promote the minimised input to the corpus so CI catches regressions
cp fuzz/artifacts/fuzz_state_transition/crash-abc123-minimised \
   fuzz/corpus/fuzz_state_transition/regression_001

Adding a New Target

# Scaffold everything automatically (corpus dir, .rs file, Cargo.toml entry, CI matrix entry)
just new-target my_feature   # creates fuzz_my_feature

just new-target calls scripts/add_fuzz_target.py, which appends the [[bin]] entry to fuzz/Cargo.toml and inserts the target into every strategy matrix in .github/workflows/fuzz.yml.


🧹 Housekeeping

Command Removes
just clean Cargo build artefacts (target/ and fuzz/target/)
just clean-artifacts fuzz/artifacts/ (crash/timeout inputs)
just clean-coverage fuzz/coverage/ (LLVM coverage reports)
just clean-all All of the above

⚙️ CI

GitHub Actions runs these workflows on every push/PR and nightly:

Workflow What it does
fuzz.ymlsmoke-fuzz (matrix) Builds + runs each libFuzzer target for 60 s
fuzz.ymlregression (matrix) Replays the saved corpus (-runs=0)
fuzz.ymlproptest cargo test -p fuzz_props --release
fuzz.ymlperf-baseline (nightly only) Measures exec/sec per target, uploads perf_baseline.txt
fuzz-afl.yml AFL++ lane over the same targets/corpus
mutants.yml Mutation testing (cargo-mutants)
lint.yml Formatting + Clippy

📖 Documentation

The full developer guide — how to add new targets, interpret crashes, update the LEZ sibling clone, and tune performance — lives in docs/fuzzing.md.


📜 License

Licensed under the MIT License.