diff --git a/.github/workflows/fuzz-afl.yml b/.github/workflows/fuzz-afl.yml index 60d31bd..039ae91 100644 --- a/.github/workflows/fuzz-afl.yml +++ b/.github/workflows/fuzz-afl.yml @@ -41,6 +41,11 @@ jobs: - fuzz_transaction_decoding - fuzz_validate_execute_consistency - fuzz_witness_set_verification + - fuzz_merkle_tree + - fuzz_transaction_properties + - fuzz_privacy_preserving_witness + - fuzz_encoding_privacy_preserving + - fuzz_nullifier_set_roundtrip steps: - name: Checkout repository @@ -239,7 +244,7 @@ jobs: if-no-files-found: ignore # ──────────────────────────────────────────────────────────────────────────── - # afl-coverage-aggregate — single HTML report merging all 15 targets + # afl-coverage-aggregate — single HTML report merging all 20 targets # ──────────────────────────────────────────────────────────────────────────── afl-coverage-aggregate: name: "AFL++ coverage — aggregated" @@ -302,6 +307,11 @@ jobs: fuzz_transaction_decoding fuzz_validate_execute_consistency fuzz_witness_set_verification + fuzz_merkle_tree + fuzz_transaction_properties + fuzz_privacy_preserving_witness + fuzz_encoding_privacy_preserving + fuzz_nullifier_set_roundtrip ) for TARGET in "${TARGETS[@]}"; do cargo build \ @@ -330,6 +340,11 @@ jobs: fuzz_transaction_decoding fuzz_validate_execute_consistency fuzz_witness_set_verification + fuzz_merkle_tree + fuzz_transaction_properties + fuzz_privacy_preserving_witness + fuzz_encoding_privacy_preserving + fuzz_nullifier_set_roundtrip ) PROFRAW_DIR="coverage/afl/aggregated/profraw" mkdir -p "$PROFRAW_DIR" @@ -409,6 +424,11 @@ jobs: fuzz_transaction_decoding fuzz_validate_execute_consistency fuzz_witness_set_verification + fuzz_merkle_tree + fuzz_transaction_properties + fuzz_privacy_preserving_witness + fuzz_encoding_privacy_preserving + fuzz_nullifier_set_roundtrip ) # llvm-cov show: first binary is a positional arg; the rest use --object first=1 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 4deb722..4970f7b 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -35,6 +35,11 @@ jobs: - fuzz_apply_state_diff_split_path - fuzz_multi_block_state_sequence - fuzz_sequencer_vs_replayer + - fuzz_merkle_tree + - fuzz_transaction_properties + - fuzz_privacy_preserving_witness + - fuzz_encoding_privacy_preserving + - fuzz_nullifier_set_roundtrip steps: - uses: actions/checkout@v4 @@ -218,6 +223,11 @@ jobs: - fuzz_apply_state_diff_split_path - fuzz_multi_block_state_sequence - fuzz_sequencer_vs_replayer + - fuzz_merkle_tree + - fuzz_transaction_properties + - fuzz_privacy_preserving_witness + - fuzz_encoding_privacy_preserving + - fuzz_nullifier_set_roundtrip steps: - uses: actions/checkout@v4 - name: Checkout logos-execution-zone @@ -285,7 +295,12 @@ jobs: fuzz_program_deployment_lifecycle \ fuzz_apply_state_diff_split_path \ fuzz_multi_block_state_sequence \ - fuzz_sequencer_vs_replayer; do + fuzz_sequencer_vs_replayer \ + fuzz_merkle_tree \ + fuzz_transaction_properties \ + fuzz_privacy_preserving_witness \ + fuzz_encoding_privacy_preserving \ + fuzz_nullifier_set_roundtrip; do echo "=== $target ===" | tee -a perf_baseline.txt cargo fuzz run "$target" -- -max_total_time=30 2>&1 \ | grep -E "exec/s|execs_per_sec" | tail -1 | tee -a perf_baseline.txt diff --git a/README.md b/README.md index bc5b4a9..6675d3e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@ -# Lez-fuzzing +
-Coverage-guided fuzzing and adversarial testing infrastructure for the -**Logos Execution Zone (LEZ)** protocol. +# 🦀 Lez-fuzzing + +**Coverage-guided fuzzing & adversarial testing infrastructure for the +[Logos Execution Zone (LEZ)](https://github.com/) protocol.** + +[![Rust](https://img.shields.io/badge/rust-nightly-orange?logo=rust)](rust-toolchain.toml) +[![Fuzzing](https://img.shields.io/badge/libFuzzer%20%C2%B7%20AFL%2B%2B-20%20targets-blue)](#-fuzz-targets) +[![Mutation testing](https://img.shields.io/badge/cargo--mutants-enabled-green)](.github/workflows/mutants.yml) +[![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE-MIT) + +
--- -## Repository Layout +## 📂 Repository Layout ``` lez-fuzzing/ @@ -22,28 +31,9 @@ lez-fuzzing/ │ └── generators.rs # Arbitrary / proptest strategies ├── fuzz/ # cargo-fuzz crate (own [workspace] sentinel) │ ├── Cargo.toml -│ ├── fuzz_targets/ -│ │ ├── _template.rs # Template for just new-target -│ │ ├── fuzz_transaction_decoding.rs -│ │ ├── fuzz_stateless_verification.rs -│ │ ├── fuzz_state_transition.rs -│ │ ├── fuzz_block_verification.rs -│ │ ├── fuzz_encoding_roundtrip.rs -│ │ ├── fuzz_signature_verification.rs -│ │ ├── fuzz_replay_prevention.rs -│ │ ├── fuzz_state_diff_computation.rs -│ │ ├── fuzz_validate_execute_consistency.rs -│ │ ├── fuzz_state_serialization.rs -│ │ ├── fuzz_witness_set_verification.rs -│ │ ├── fuzz_program_deployment_lifecycle.rs -│ │ ├── fuzz_apply_state_diff_split_path.rs -│ │ ├── fuzz_multi_block_state_sequence.rs -│ │ ├── fuzz_sequencer_vs_replayer.rs -│ │ ├── fuzz_merkle_tree.rs -│ │ ├── fuzz_transaction_properties.rs -│ │ ├── fuzz_privacy_preserving_witness.rs -│ │ ├── fuzz_encoding_privacy_preserving.rs -│ │ └── fuzz_nullifier_set_roundtrip.rs # 20 targets total — see table below +│ ├── fuzz_targets/ # 20 targets total — see table below +│ │ ├── _template.rs # Template for `just new-target` +│ │ └── fuzz_*.rs │ └── corpus/ # Curated seed inputs (one dir per target) ├── .github/ │ └── workflows/ @@ -54,11 +44,12 @@ lez-fuzzing/ ├── scripts/ │ └── add_fuzz_target.py # Automates new-target scaffolding (called by just new-target) └── docs/ - └── fuzzing.md # Full developer guide + ├── 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: +`logos-execution-zone` next to this repository so the `../` path deps resolve: ``` parent/ @@ -68,25 +59,25 @@ parent/ --- -## Quick Start +## 🚀 Quick Start -### Prerequisites +### 1. Prerequisites ```bash rustup install nightly rustup component add llvm-tools-preview --toolchain nightly cargo install cargo-fuzz -# Optional but recommended: -cargo install just +cargo install just # optional but recommended ``` +> [!NOTE] > **Why nightly?** `cargo-fuzz` passes `-Zsanitizer=address` and > `-Zinstrument-coverage` (unstable flags) to `rustc`, and depends on the -> `llvm-tools-preview` nightly component for coverage reporting. The -> `rust-toolchain.toml` pins the whole repository to nightly so you never +> `llvm-tools-preview` nightly component for coverage reporting. +> `rust-toolchain.toml` pins the whole repository to nightly, so you never > need an explicit `+nightly` flag. -### Setup +### 2. Setup ```bash # Clone both repositories side by side @@ -95,7 +86,7 @@ git clone lez-fuzzing cd lez-fuzzing ``` -### Run the fuzz targets +### 3. Run the fuzz targets ```bash # All targets for 30 s each (RISC0_DEV_MODE=1 is set automatically) @@ -114,38 +105,42 @@ just fuzz-regression 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. +> generation. Without it, each execution takes **seconds** instead of +> **microseconds**. --- -## Fuzz Targets +## 🎯 Fuzz Targets -| Target | Protocol layer | Entry point | -|--------|---------------|-------------| -| `fuzz_transaction_decoding` | Borsh decoding of all tx/block types (`LeeTransaction`, `Block`, `HashableBlockData`) with roundtrip re-encoding | `fuzz/fuzz_targets/fuzz_transaction_decoding.rs` | -| `fuzz_stateless_verification` | `transaction_stateless_check()` no-panic + idempotency | `fuzz/fuzz_targets/fuzz_stateless_verification.rs` | -| `fuzz_state_transition` | `V03State` transition: StateIsolationOnFailure + BalanceConservation + ReplayRejection invariants across up to 8 txs with fuzz-driven state | `fuzz/fuzz_targets/fuzz_state_transition.rs` | -| `fuzz_block_verification` | Block hash integrity: HashRoundTrip · HashPreimage completeness (block_id/prev_hash/timestamp) · TxOrderCommitment | `fuzz/fuzz_targets/fuzz_block_verification.rs` | -| `fuzz_encoding_roundtrip` | Borsh encode→decode→encode round-trip identity + canonical encoding for `PublicTransaction` and `ProgramDeploymentTransaction` | `fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs` | -| `fuzz_signature_verification` | Signature correctness (sign→verify), no-panic on random bytes, cross-key soundness | `fuzz/fuzz_targets/fuzz_signature_verification.rs` | -| `fuzz_replay_prevention` | Transaction nonce replay rejection with fuzz-driven initial state | `fuzz/fuzz_targets/fuzz_replay_prevention.rs` | -| `fuzz_state_diff_computation` | `ValidatedStateDiff` forward containment + reverse completeness (bidirectional isolation check) | `fuzz/fuzz_targets/fuzz_state_diff_computation.rs` | -| `fuzz_validate_execute_consistency` | `validate_on_state` / `execute_check_on_state` agreement + diff accuracy + BalanceConservation | `fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs` | -| `fuzz_state_serialization` | `V03State` Borsh decode no-panic + StateSerializationRoundtrip idempotency + NullifierDeduplication (`NullifierSet` hand-written impl) | `fuzz/fuzz_targets/fuzz_state_serialization.rs` | -| `fuzz_witness_set_verification` | `WitnessSet::is_valid_for` no-panic + CorrectVerification (sign→verify) + MessageIsolation (witness set for msg A rejected on msg B) | `fuzz/fuzz_targets/fuzz_witness_set_verification.rs` | -| `fuzz_program_deployment_lifecycle` | `V03State::transition_from_program_deployment_transaction` no-panic + BalanceIsolation (deployment must not move tokens) + StateIsolationOnFailure | `fuzz/fuzz_targets/fuzz_program_deployment_lifecycle.rs` | -| `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 | `fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs` | -| `fuzz_multi_block_state_sequence` | LongRangeBalanceConservation across up to 16 blocks + FailedTxNonceStability (nonce must not change on rejection) + PerBlockReplayRejection | `fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs` | -| `fuzz_sequencer_vs_replayer` | Differential: sequencer path (`validate_on_state` → `apply_state_diff`) vs replayer path (`execute_check_on_state`) — SequencerReplayerEquivalence + ReplayerAcceptsAllSequencerTxs + ClockConsistency | `fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs` | -| `fuzz_merkle_tree` | Commitment Merkle tree via the commitment set: ProofSome · ProofValid (leaf + auth path recomputes the root) · NonMembershipNone · IndicesSequential | `fuzz/fuzz_targets/fuzz_merkle_tree.rs` | -| `fuzz_transaction_properties` | Transaction property invariants: HashDeterministic/HashNonDefault, SignerIds derived from witness keys & non-empty, AffectedAccountsContainSigners, PublicDiffNonEmptyOnSuccess | `fuzz/fuzz_targets/fuzz_transaction_properties.rs` | -| `fuzz_privacy_preserving_witness` | `privacy_preserving_transaction::WitnessSet`: CorrectVerification (witness for msg A passes `signatures_are_valid_for(A)`) + MessageIsolation + SignerIdsMatchWitnessKeys | `fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs` | -| `fuzz_encoding_privacy_preserving` | Privacy-preserving encoding: MessageEncodingRoundtrip + TxEncodingDeterministic/NonEmpty | `fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs` | -| `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: NullifierSetRoundtrip (decode→encode identity for the hand-written impl) | `fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs` | +| # | 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_state` → `apply_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) | +Each target lives at `fuzz/fuzz_targets/.rs`. + +> [!NOTE] > **Input-independent checks are not fuzz targets here.** Deterministic invariants > that ignore their input (e.g. genesis-account contents, getter/round-trip > identities, the system-account-modification guard) belong in `logos-execution-zone` @@ -155,7 +150,7 @@ just fuzz-props --- -## Corpus Management +## 🧬 Corpus Management ```bash # Minimise all corpora (removes dominated inputs, keeps coverage-equivalent set) @@ -167,52 +162,52 @@ just corpus-cmin-target fuzz_state_transition --- -## Crash / Failure Workflow +## 💥 Crash / Failure Workflow ```bash -# Minimise a crash artifact +# 1. Minimise a crash artifact just fuzz-tmin fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-abc123 -# Print the bytes as a Rust literal (for a regression #[test]) +# 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 -# Promote the minimised input to the corpus so CI catches regressions +# 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 +## ➕ Adding a New Target ```bash # 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`](scripts/add_fuzz_target.py) which +`just new-target` calls [`scripts/add_fuzz_target.py`](scripts/add_fuzz_target.py), which appends the `[[bin]]` entry to [`fuzz/Cargo.toml`](fuzz/Cargo.toml) and inserts the target into every strategy matrix in [`.github/workflows/fuzz.yml`](.github/workflows/fuzz.yml). --- -## Housekeeping +## 🧹 Housekeeping -```bash -just clean # Remove Cargo build artefacts (target/ and fuzz/target/) -just clean-artifacts # Remove fuzz/artifacts/ (crash/timeout inputs) -just clean-coverage # Remove fuzz/coverage/ (LLVM coverage reports) -just clean-all # All of the above -``` +| 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 +## ⚙️ CI GitHub Actions runs these workflows on every push/PR and nightly: | Workflow | What it does | -|----------|-------------| +|----------|--------------| | `fuzz.yml` — `smoke-fuzz` (matrix) | Builds + runs each libFuzzer target for 60 s | | `fuzz.yml` — `regression` (matrix) | Replays the saved corpus (`-runs=0`) | | `fuzz.yml` — `proptest` | `cargo test -p fuzz_props --release` | @@ -221,22 +216,23 @@ GitHub Actions runs these workflows on every push/PR and nightly: | `mutants.yml` | Mutation testing (`cargo-mutants`) | | `lint.yml` | Formatting + Clippy | -> **Note:** The `fuzz.yml` matrix currently lists 15 of the 20 libFuzzer targets. -> Still missing: `fuzz_merkle_tree`, `fuzz_transaction_properties`, -> `fuzz_privacy_preserving_witness`, `fuzz_encoding_privacy_preserving`, and -> `fuzz_nullifier_set_roundtrip` — add them to `.github/workflows/fuzz.yml`. See -> [`docs/fuzzing.md`](docs/fuzzing.md) for the manual fallback instructions. +> [!NOTE] +> All **20** libFuzzer targets are wired into every `fuzz.yml` matrix +> (smoke-fuzz · regression · perf-baseline), the `fuzz-afl.yml` AFL++ lane, and +> the `mutants.yml` corpus-replay job. New targets are added automatically by +> `just new-target`; see [`docs/fuzzing.md`](docs/fuzzing.md) for the manual +> fallback instructions. --- -## Documentation +## 📖 Documentation -Full developer guide — how to add new targets, interpret crashes, update -the LEZ sibling clone, and tune performance — is in +The full developer guide — how to add new targets, interpret crashes, update +the LEZ sibling clone, and tune performance — lives in [`docs/fuzzing.md`](docs/fuzzing.md). --- -## License +## 📜 License Licensed under the [MIT License](LICENSE-MIT). diff --git a/current_vs_alternative_approach.md b/current_vs_alternative_approach.md index ba97b25..e20982c 100644 --- a/current_vs_alternative_approach.md +++ b/current_vs_alternative_approach.md @@ -1,6 +1,6 @@ # Alternative Approaches vs. Current Implementation -## What the Current Project Does +## 🧩 What the Current Project Does The `lez-fuzzing` repository is a **coverage-guided, structured mutation fuzzing system** built on **cargo-fuzz / libFuzzer**, operating as a standalone companion to the Logos Execution Zone (LEZ) codebase. Its key design pillars: @@ -17,7 +17,7 @@ The `lez-fuzzing` repository is a **coverage-guided, structured mutation fuzzing --- -## Alternative Approaches +## 🔬 Alternative Approaches ### 1. AFL++ (American Fuzzy Lop++) @@ -127,7 +127,7 @@ fuzzing replacement. --- -## Summary Comparison Matrix +## 📊 Summary Comparison Matrix | Approach | Bug-finding depth | CI cost | Impl. cost | Complements current? | Recommended action | |---|---|---|---|---|---| @@ -141,7 +141,7 @@ fuzzing replacement. --- -## Decision-maker Recommendations +## 🧭 Decision-maker Recommendations **Already done** (was previously recommended here): @@ -150,16 +150,16 @@ fuzzing replacement. with the Plane A / Plane B framework documented in [`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md). - ✅ **Differential testing** — `fuzz_sequencer_vs_replayer`. +- ✅ **Complete `fuzz.yml` CI matrix** — all **20** libFuzzer targets now run in + every `fuzz.yml` matrix (smoke-fuzz · regression · perf-baseline) and the + `fuzz-afl.yml` AFL++ lane, including `fuzz_merkle_tree`, + `fuzz_transaction_properties`, `fuzz_privacy_preserving_witness`, + `fuzz_encoding_privacy_preserving`, and `fuzz_nullifier_set_roundtrip`. **Remaining higher-ROI next steps, in priority order:** -1. **Finish the `fuzz.yml` CI matrix** — it lists 15 of the 20 libFuzzer targets; - add `fuzz_merkle_tree`, `fuzz_transaction_properties`, - `fuzz_privacy_preserving_witness`, `fuzz_encoding_privacy_preserving`, and - `fuzz_nullifier_set_roundtrip`. - -2. **Honggfuzz on Linux CI only** — hardware-counter coverage finds different paths; +1. **Honggfuzz on Linux CI only** — hardware-counter coverage finds different paths; gated to Linux since Apple Silicon has no HW counters. -3. **Formal verification of core invariants** (balance conservation, replay +2. **Formal verification of core invariants** (balance conservation, replay prevention) — a long-term supplement, not a replacement. \ No newline at end of file diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 5c3579e..a714da0 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -1,4 +1,10 @@ -# Fuzzing Guide +
+ +# 🔬 Fuzzing Guide + +**The full developer guide to running, extending, and triaging the LEZ fuzzing infrastructure.** + +
This document covers how to run fuzz targets, add new targets, minimise failures, and convert findings into regression tests. @@ -9,7 +15,7 @@ directory that must be cloned separately). --- -## Architecture +## 🏗️ Architecture The fuzz workspace (`fuzz/`) is a single Cargo workspace that covers **both** fuzzing engines via Cargo features. No separate Cargo manifest is needed. @@ -38,7 +44,7 @@ The `cfg` attributes in the macro expansion resolve against the **calling crate' --- -## Prerequisites +## 🧰 Prerequisites ```bash # libFuzzer lane @@ -61,7 +67,7 @@ cargo install cargo-afl --- -## Repository Setup +## 📁 Repository Setup `lez-fuzzing` is a **standalone repository** — it does **not** use git submodules. It expects the LEZ codebase to be cloned at `../logos-execution-zone` relative to itself. @@ -79,7 +85,7 @@ git clone lez-fuzzing --- -## How to Run +## ▶️ How to Run All fuzz targets must be run with `RISC0_DEV_MODE=1` to disable expensive ZK proof generation. The `just` recipes handle this automatically. @@ -99,7 +105,7 @@ just fuzz-regression --- -## Available Fuzz Targets +## 🎯 Available Fuzz Targets | Target | What it fuzzes | Entry point | |--------|---------------|-------------| @@ -118,10 +124,15 @@ just fuzz-regression | `fuzz_apply_state_diff_split_path` | **SplitPathEquivalence**: for every known account, `validate_on_state` + `apply_state_diff` must produce exactly the same balance, nonce, data, and program_owner as `execute_check_on_state`; **NonceIncrementCorrectness**: nonce after the split path equals nonce after the direct path for all signer accounts (catches bugs in the two-step `apply_state_diff` nonce-increment logic) | `fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs` | | `fuzz_multi_block_state_sequence` | **LongRangeBalanceConservation**: total genesis-account balance identical before and after all N (≤ 16) blocks; **FailedTxNonceStability**: every genesis-account nonce unchanged after a rejected transaction; **PerBlockReplayRejection**: every transaction accepted in block B is rejected in block B+1 (cumulative nonce-interaction coverage) | `fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs` | | `fuzz_sequencer_vs_replayer` | **SequencerReplayerEquivalence**: for every known account (genesis ∪ diff-declared), the sequencer path (`validate_on_state` → `apply_state_diff`) and the replayer path (`execute_check_on_state`) must produce identical balance, nonce, data, and program_owner after applying a full block of up to 8 transactions plus the mandatory clock invocation; **ReplayerAcceptsAllSequencerTxs**: every transaction accepted by `validate_on_state` must also be accepted by `execute_check_on_state`; **ClockConsistency**: the mandatory clock invocation must succeed on both paths and leave both states identical | `fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs` | +| `fuzz_merkle_tree` | Commitment Merkle tree via the commitment set: **ProofSome**, **ProofValid** (leaf + auth path recomputes the root), **NonMembershipNone**, **IndicesSequential** | `fuzz/fuzz_targets/fuzz_merkle_tree.rs` | +| `fuzz_transaction_properties` | Transaction property invariants: **HashDeterministic** / **HashNonDefault**, **SignerIds** derived from witness keys & non-empty, **AffectedAccountsContainSigners**, **PublicDiffNonEmptyOnSuccess** | `fuzz/fuzz_targets/fuzz_transaction_properties.rs` | +| `fuzz_privacy_preserving_witness` | `privacy_preserving_transaction::WitnessSet`: **CorrectVerification** (witness for message A passes `signatures_are_valid_for(A)`), **MessageIsolation**, **SignerIdsMatchWitnessKeys** | `fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs` | +| `fuzz_encoding_privacy_preserving` | Privacy-preserving encoding: **MessageEncodingRoundtrip**, **TxEncodingDeterministic** / **NonEmpty** | `fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs` | +| `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: **NullifierSetRoundtrip** (decode→encode identity for the hand-written impl) | `fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs` | --- -## How to Add a New Fuzz Target +## ➕ How to Add a New Fuzz Target ### Step 1 — Scaffold with `just new-target` @@ -159,6 +170,7 @@ which: - Inserts the target name into every strategy matrix and the perf-baseline shell loop in [`.github/workflows/fuzz.yml`](../.github/workflows/fuzz.yml). +> [!TIP] > **Manual fallback:** if you create a target without `just new-target`, add the > entry yourself: > @@ -197,12 +209,13 @@ cd fuzz && cargo afl build \ --- -## AFL++ Parallel Fuzzing Lane +## 🔀 AFL++ Parallel Fuzzing Lane ### Prerequisites Install AFL++ natively on your machine. +> [!NOTE] > **Note on Linux package versions**: The `afl++` package in Debian stable (Bookworm) > and Ubuntu LTS is several major versions behind the current AFL++ 4.x series and may > be incompatible with `cargo-afl`. **Build from source** for a current version. @@ -222,6 +235,7 @@ cd .. cargo install cargo-afl ``` +> [!IMPORTANT] > **macOS: run `afl-system-config` once before fuzzing** — AFL++ uses System V shared > memory (`shmget`) to pass coverage bitmaps between the fuzzer and the target. macOS > ships with very small defaults (`kern.sysv.shmmax = 4 MB`, `kern.sysv.shmmni = 32`) @@ -245,6 +259,7 @@ cargo install cargo-afl > each restart. The `just fuzz-afl` and `just fuzz-afl-parallel` recipes **do not** > call this automatically because it requires `sudo`. +> [!IMPORTANT] > **macOS: crash reporter must be disabled** — AFL++ detects the macOS `ReportCrash` > daemon and aborts if it is active (it delays crash notifications and causes AFL++ to > mis-classify crashes as timeouts). The `just fuzz-afl` and `just fuzz-afl-parallel` @@ -361,24 +376,24 @@ The nightly AFL++ CI workflow has two jobs: | Job | Triggers | Matrix | |-----|----------|--------| -| `afl-smoke` | nightly + `workflow_dispatch` | 7 priority targets, 120 s each | -| `afl-coverage` | nightly, `needs: afl-smoke` | 3 key targets; LLVM HTML report | +| `afl-smoke` | nightly + `workflow_dispatch` | all 20 targets, 60 s each | +| `afl-coverage-aggregate` | nightly, `needs: afl-smoke` | all 20 targets merged into one LLVM HTML report | -The smoke job: -1. Builds the target with `cargo afl build --no-default-features --features fuzzer-afl` -2. Runs `afl-fuzz` for 120 s in `aflplusplus/aflplusplus:v4.40c` container -3. Syncs new queue entries into `fuzz/corpus//` and opens a corpus PR -4. Uploads crashes/hangs as a workflow artifact +The smoke job (one matrix leg per target, on `ubuntu-latest`): +1. Builds AFL++ from source, then builds the target with `cargo afl build --no-default-features --features fuzzer-afl` +2. Runs `afl-fuzz` for 60 s (`timeout 60`) +3. Reports edge-bitmap coverage to the job step summary +4. Uploads the queue/crashes/hangs as a workflow artifact -The coverage job: -1. Downloads the smoke findings -2. Rebuilds with `RUSTFLAGS="-C instrument-coverage"` -3. Runs all corpus + queue inputs through the binary -4. Merges `.profraw` → `.profdata` → HTML report via `llvm-cov show` +The coverage-aggregate job: +1. Downloads every smoke leg's findings +2. Rebuilds all 20 targets with `RUSTFLAGS="-C instrument-coverage"` +3. Runs all checked-in corpus + AFL queue inputs through each binary +4. Merges every `.profraw` → one `.profdata` → a single combined HTML report via `llvm-cov show` --- -## Updating the LEZ Dependency +## 🔄 Updating the LEZ Dependency `lez-fuzzing` reads LEZ source directly from `../logos-execution-zone`. To pick up LEZ changes, simply update that repo: @@ -401,7 +416,7 @@ just update-lez --- -## Minimising & Reproducing Failures +## 🐛 Minimising & Reproducing Failures When `cargo fuzz` finds a crash it writes an artifact to `fuzz/artifacts/fuzz_/crash-`. @@ -440,7 +455,7 @@ Open a PR. The `regression` CI job will permanently block re-introduction of thi --- -## Coverage Reports +## 📊 Coverage Reports ### Step 1 — libFuzzer coverage (via `cargo fuzz coverage`) @@ -476,7 +491,7 @@ automates steps 2–5 and uploads the report as a workflow artifact. --- -## Invariant Framework +## 🛡️ Invariant Framework Shared invariants live in `fuzz_props/src/invariants.rs`. There are two layers: @@ -538,7 +553,7 @@ To add a new invariant: --- -## Input Generators +## 🎲 Input Generators The `fuzz_props` crate provides two layers of input generation: @@ -579,7 +594,7 @@ fuzz target parameters for zero-boilerplate structured fuzzing. --- -## Performance Baseline +## ⚡ Performance Baseline Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`: @@ -600,7 +615,13 @@ Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`: | `fuzz_apply_state_diff_split_path` | ~5 000 exec/sec *(estimate)* | | `fuzz_multi_block_state_sequence` | ~1 000 exec/sec *(estimate)* | | `fuzz_sequencer_vs_replayer` | ~2 000 exec/sec *(estimate)* | +| `fuzz_merkle_tree` | ~20 000 exec/sec *(estimate)* | +| `fuzz_transaction_properties` | ~15 000 exec/sec *(estimate)* | +| `fuzz_privacy_preserving_witness` | ~15 000 exec/sec *(estimate)* | +| `fuzz_encoding_privacy_preserving` | ~50 000 exec/sec *(estimate)* | +| `fuzz_nullifier_set_roundtrip` | ~100 000 exec/sec *(estimate)* | +> [!NOTE] > Throughput figures for the five new targets are rough estimates; run `just perf-baseline` > locally or check the `perf-baseline` CI artifact for up-to-date measurements. @@ -617,7 +638,7 @@ just fuzz-afl-parallel fuzz_state_transition $(nproc) 3600 --- -## ZK-Proof Cost Warning +## ⚠️ ZK-Proof Cost Warning `PrivacyPreservingTransaction` uses `risc0-zkvm` (seconds per proof). All fuzz targets **must** set `RISC0_DEV_MODE=1` in the environment and the `just` @@ -632,7 +653,7 @@ flag stubs out ZK proof generation and replaces it with a fast mock implementati --- -## Mutation testing — the two planes +## 🧬 Mutation testing — the two planes Mutation testing here runs in two distinct planes, answering two different questions: @@ -666,7 +687,7 @@ from `data`; if a check doesn't depend on the input, write it as a unit test in --- -## Known Limitations & Future Work +## 🚧 Known Limitations & Future Work | Item | Notes | |------|-------| diff --git a/docs/mutants-not-fuzzable.md b/docs/mutants-not-fuzzable.md index 0efdf03..e703da9 100644 --- a/docs/mutants-not-fuzzable.md +++ b/docs/mutants-not-fuzzable.md @@ -35,7 +35,7 @@ mutant on **neither** list warrants a new corpus input. --- -## Why fuzzing is the wrong tool for these +## 🧭 Why fuzzing is the wrong tool for these Fuzzing earns its keep by exploring a large, *unknown* input space to find inputs a human wouldn't think of — malformed transactions, adversarial byte sequences, @@ -74,7 +74,7 @@ to new unit tests — see the companion doc) were all removed. --- -## Catalogue (Group 1 — structurally unreachable by fuzzing) +## 📋 Catalogue (Group 1 — structurally unreachable by fuzzing) The nine mutations reported as MISSED by the `mutants-protocol` run for which fuzzing is structurally the wrong tool, with their true coverage. Verified by @@ -143,7 +143,8 @@ fuzz input can reach this code. The `lee` crate exercises them directly. `state::tests::public_changer_claimer_data_change_no_claim_fails` and `public_changer_claimer_no_data_change_no_claim_succeeds`. -> Note: an earlier analysis guessed 6 and 7 were *equivalent mutants*. They are +> [!NOTE] +> an earlier analysis guessed 6 and 7 were *equivalent mutants*. They are > not — they are caught by Plane A, just not reachable by Plane B. They appear > "equivalent" only if you restrict yourself to the deployed `authenticated_transfer` > program, which is exactly the restriction fuzzing operates under. @@ -165,7 +166,7 @@ fuzz input can reach this code. The `lee` crate exercises them directly. --- -## Group 2 — migrated input-independent targets +## 🔁 Group 2 — migrated input-independent targets These mutants used to be caught by Plane B via input-independent fuzz targets. Those targets were removed and their invariants ported to LEZ unit tests, so the @@ -207,7 +208,7 @@ port *added* coverage rather than merely relocating it; those are marked **(new) --- -## Re-verifying +## ✅ Re-verifying From `logos-execution-zone/` with the fuzzing repo checked out as a sibling: @@ -226,6 +227,7 @@ registry; a mutation that the corpus replay (`just mutants-protocol`) catches belongs in the corpus instead. Across both groups, mutation #4 (the near-equivalent cycle-limit weak mutant) is the only one caught by **neither** plane. -> Tip: when reverting, prefer reverse-editing only the mutated line rather than +> [!TIP] +> when reverting, prefer reverse-editing only the mutated line rather than > `git checkout -- ` if you have uncommitted unit tests in the same file — > a whole-file checkout would discard them too.