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-toolchain.toml)
+[](#-fuzz-targets)
+[](.github/workflows/mutants.yml)
+[](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.