From 06e5e2e8434f973b34126b9c84f9590e2f97635f Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 14 May 2026 10:55:01 +0800 Subject: [PATCH] fix: update documentation --- README.md | 22 +- colocated_vs_separated.md | 108 ------ current_vs_alternative_approach.md | 12 +- docs/fuzzing.md | 50 ++- presentation_qa_team.md | 509 ----------------------------- targets_coverage.md | 20 -- 6 files changed, 51 insertions(+), 670 deletions(-) delete mode 100644 colocated_vs_separated.md delete mode 100644 presentation_qa_team.md delete mode 100644 targets_coverage.md diff --git a/README.md b/README.md index 336a74f..d3908f6 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ lez-fuzzing/ ├── Justfile # Turn-key entry-points ├── rust-toolchain.toml # Pins Rust nightly (required by cargo-fuzz) ├── .gitignore -├── scripts/ -│ └── add_fuzz_target.py # Automates new-target scaffolding (called by just new-target) ├── fuzz_props/ # Shared invariant framework + input generators │ ├── Cargo.toml │ └── src/ @@ -39,6 +37,8 @@ lez-fuzzing/ ├── .github/ │ └── workflows/ │ └── fuzz.yml # CI: smoke-fuzz · regression · proptest · perf +├── scripts/ +│ └── add_fuzz_target.py # Automates new-target scaffolding (called by just new-target) └── docs/ └── fuzzing.md # Full developer guide ``` @@ -111,15 +111,15 @@ just fuzz-props | Target | Protocol layer | Entry point | |--------|---------------|-------------| -| `fuzz_transaction_decoding` | Borsh decoding of all tx/block types | `fuzz/fuzz_targets/fuzz_transaction_decoding.rs` | -| `fuzz_stateless_verification` | `transaction_stateless_check()` idempotency | `fuzz/fuzz_targets/fuzz_stateless_verification.rs` | -| `fuzz_state_transition` | `V03State` transition + state-isolation invariant | `fuzz/fuzz_targets/fuzz_state_transition.rs` | -| `fuzz_block_verification` | Block hash integrity | `fuzz/fuzz_targets/fuzz_block_verification.rs` | -| `fuzz_encoding_roundtrip` | Borsh encode→decode→encode round-trip identity | `fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs` | -| `fuzz_signature_verification` | Signature creation + verification correctness and no-panic | `fuzz/fuzz_targets/fuzz_signature_verification.rs` | -| `fuzz_replay_prevention` | Transaction nonce replay rejection | `fuzz/fuzz_targets/fuzz_replay_prevention.rs` | -| `fuzz_state_diff_computation` | `ValidatedStateDiff` scope isolation (only declared accounts mutated) | `fuzz/fuzz_targets/fuzz_state_diff_computation.rs` | -| `fuzz_validate_execute_consistency` | `validate_on_state` / `execute_check_on_state` agreement + diff accuracy | `fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs` | +| `fuzz_transaction_decoding` | Borsh decoding of all tx/block types (`NSSATransaction`, `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` | --- diff --git a/colocated_vs_separated.md b/colocated_vs_separated.md deleted file mode 100644 index 4425b0d..0000000 --- a/colocated_vs_separated.md +++ /dev/null @@ -1,108 +0,0 @@ -## Co-located vs. Separate Repository for LEZ Fuzzing - -### The Core Problem with the Current Setup - -[`docs/fuzzing.md:275`](docs/fuzzing.md:275) explicitly acknowledges the critical risk: - -> "There is no submodule pin — `lez-fuzzing` reads `../logos-execution-zone` as checked out." - -This means the two repositories can silently diverge. A LEZ API change will break `fuzz/Cargo.toml`'s path dependencies (`path = "../../logos-execution-zone/nssa"`) without any automated guard. A developer with a stale LEZ checkout will fuzz the wrong code version. - ---- - -### What Co-location Would Look Like - -The standard `cargo fuzz` convention places the fuzz workspace **inside** the target repo: - -``` -logos-execution-zone/ -├── nssa/ -├── common/ -├── fuzz_props/ ← moved in as optional workspace member -│ └── src/ -├── fuzz/ ← standard cargo fuzz location -│ ├── Cargo.toml ← [workspace] breaks out of parent workspace -│ ├── rust-toolchain.toml ← pins nightly for this sub-workspace only -│ └── fuzz_targets/ -└── Cargo.toml ← parent workspace (stable toolchain) -``` - -The `[workspace]` declaration in [`fuzz/Cargo.toml`](fuzz/Cargo.toml:11) already does exactly this break-out — the only structural change is moving the directory into LEZ. - ---- - -### Detailed Trade-off Analysis - -#### Benefits of Co-location (moving into `logos-execution-zone/`) - -| Benefit | Detail | -|---|---| -| **Zero version drift** | Fuzz targets and production code are in the same commit graph — they are always in sync by construction | -| **Atomic API changes** | A PR that renames a LEZ method updates the fuzz target in the same diff; currently a LEZ PR can silently break `lez-fuzzing` | -| **Single clone onboarding** | Currently requires cloning two repos in an exact directory layout ([`docs/fuzzing.md:29-37`](docs/fuzzing.md:29)); co-location needs one | -| **Standard convention** | `cargo fuzz init` places `fuzz/` inside the target repo; this is the Rust ecosystem standard (tokio, rustls, serde all do this) | -| **Feature-gate access** | [`docs/fuzzing.md:272`](docs/fuzzing.md:272) notes that `cfg(any(test, feature = "fuzzing"))` guards on `V03State` are needed to expose internal APIs for fuzzing; these work naturally within the same workspace but require cross-repo feature flag coordination when separated | -| **LEZ CI enforces fuzz compilation** | `cargo fuzz build` runs on every LEZ PR; currently a breaking LEZ change is only discovered when someone runs `lez-fuzzing` separately | -| **Simpler path dependencies** | `path = "../../logos-execution-zone/nssa"` becomes `path = "../nssa"` — no sibling-directory assumption | - -#### Costs/Risks of Co-location - -| Cost | Severity | Mitigation | -|---|---|---| -| **Nightly toolchain in LEZ CI** | Medium | Place `fuzz/rust-toolchain.toml` specifying nightly — only the `fuzz/` sub-workspace uses it; stable toolchain unchanged for all production code | -| **Corpus files in LEZ history** | Low | The ~150 corpus files are small binary blobs (~30–1600 bytes each); negligible `.git` impact. Alternatively, store corpus in a separate branch or use `cargo fuzz` corpus fetch from a CI artifact cache | -| **Fuzzing noise in main repo PRs** | Low | Fuzz targets live in `fuzz/fuzz_targets/` which is outside `src/` — reviewers can ignore them | -| **Security audit scope creep** | Low | Auditors can exclude `fuzz/` and `fuzz_props/` explicitly; fuzzing code is dev-only | - -#### Benefits of Staying Separate (current approach) - -| Benefit | Applicability | -|---|---| -| **Independent release cadence** | Valid during initial development; becomes less important as LEZ stabilises | -| **Clean LEZ commit history** | Corpus additions and fuzzing experiments don't appear in LEZ history | -| **Separate CI billing** | Fuzzing CI minutes billed to `lez-fuzzing` repo, not LEZ repo | - -#### Costs of Staying Separate - -| Cost | Current evidence | -|---|---| -| **Version drift** | Explicitly flagged as a known limitation in [`docs/fuzzing.md:275`](docs/fuzzing.md:275) with no automated enforcement | -| **Two-repo onboarding friction** | Requires exact sibling directory layout; documented but error-prone | -| **Broken fuzz builds go undetected** | A LEZ refactor that breaks fuzz targets compiles fine in LEZ CI and is only caught when `lez-fuzzing` is run separately | -| **Cross-repo `cfg` feature coordination** | [`docs/fuzzing.md:272`](docs/fuzzing.md:272) requires adding `cfg(any(test, feature = "fuzzing"))` guards in LEZ — coupling that has no enforcement mechanism across repos | - ---- - -### Architectural Options - -```mermaid -graph TD - A[Current: separate lez-fuzzing repo] -->|Version drift risk| B[Option 1: fuzz/ inside LEZ] - A -->|Keep independence| C[Option 2: Git submodule] - A -->|Minimal change| D[Option 3: CI cross-repo trigger] - - B -->|Standard cargo fuzz convention| B1["logos-execution-zone/fuzz/\n+ logos-execution-zone/fuzz_props/"] - C -->|LEZ embeds lez-fuzzing| C1["logos-execution-zone/ contains\nlez-fuzzing/ as submodule"] - D -->|LEZ PR triggers lez-fuzzing CI| D1["lez-fuzzing CI runs on\nevery LEZ commit via workflow_call"] -``` - -**Option 1 — Co-location (recommended)**: Move `fuzz/` and `fuzz_props/` into `logos-execution-zone/`. Standard convention, eliminates version drift, simplest long-term maintenance. Nightly toolchain scoped to `fuzz/rust-toolchain.toml`. - -**Option 2 — Git submodule**: `logos-execution-zone` embeds `lez-fuzzing` as a submodule. Preserves repo separation and independent history but adds submodule complexity (detached HEAD states, `git submodule update` friction). Not recommended — submodules are widely considered operationally painful. - -**Option 3 — CI cross-repo trigger**: Keep repos separate but add a GitHub Actions `workflow_call` or `repository_dispatch` that runs `lez-fuzzing` CI on every `logos-execution-zone` push. This catches compilation breakage early without merging histories. Lower migration cost than Option 1, but does not solve the onboarding problem or the `cfg` feature-gate coordination problem. - ---- - -### Recommendation - -**Co-locate (Option 1)** for a project at this stage. The version drift problem is real and already documented; the `fuzz/Cargo.toml` sub-workspace pattern already handles nightly toolchain isolation; and the `fuzz_props` crate with its `ProtocolInvariant` framework belongs logically with the protocol it tests. - -The migration is low-effort: -1. Move `fuzz/` and `fuzz_props/` into `logos-execution-zone/`. -2. Update path dependencies from `path = "../../logos-execution-zone/nssa"` to `path = "../nssa"`. -3. Add `fuzz/rust-toolchain.toml` pinning nightly. -4. Add `cargo fuzz build` smoke step to LEZ's CI workflow. -5. Archive or redirect `lez-fuzzing` with a pointer to the new location. - -The only scenario where staying separate remains preferable is if the LEZ team explicitly wants fuzzing CI costs billed separately and is disciplined about running `just update-lez` and rebuilding before every fuzzing session — which the current documentation already requires but provides no enforcement for. \ No newline at end of file diff --git a/current_vs_alternative_approach.md b/current_vs_alternative_approach.md index c711099..5b24b1b 100644 --- a/current_vs_alternative_approach.md +++ b/current_vs_alternative_approach.md @@ -32,7 +32,7 @@ The `lez-fuzzing` repository is a **coverage-guided, structured mutation fuzzing | CI ergonomics | Requires AFL++ binary in CI image | `cargo install cargo-fuzz` only | | Rust integration | `cargo-afl` | `cargo-fuzz` | -**Decision-maker view**: AFL++ and libFuzzer find *different* bugs because they use different mutation heuristics. Running both on the same corpus is the industry-standard "belt and suspenders" approach. [`docs/fuzzing.md`](docs/fuzzing.md:273) already lists `just fuzz-afl` as planned future work. **Incremental cost is low** — the same [`fuzz_props`](fuzz_props/src/lib.rs) crate and seed corpus work unchanged. +**Decision-maker view**: AFL++ and libFuzzer find *different* bugs because they use different mutation heuristics. Running both on the same corpus is the industry-standard "belt and suspenders" approach. [`docs/fuzzing.md`](docs/fuzzing.md:334) already lists `just fuzz-afl` as planned future work. **Incremental cost is low** — the same [`fuzz_props`](fuzz_props/src/lib.rs) crate and seed corpus work unchanged. --- @@ -69,9 +69,9 @@ The `lez-fuzzing` repository is a **coverage-guided, structured mutation fuzzing ### 4. Differential Fuzzing (Sequencer vs. Replayer) -**What it is**: Feed identical inputs to two independent implementations of the same interface and assert identical outputs. Already **partially implemented** in [`fuzz_validate_execute_consistency.rs`](fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs) — it compares [`validate_on_state`](fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs:35) vs. [`execute_check_on_state`](fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs:39). +**What it is**: Feed identical inputs to two independent implementations of the same interface and assert identical outputs. Already **partially implemented** in [`fuzz_validate_execute_consistency.rs`](fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs) — it compares [`validate_on_state`](fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs:61) vs. [`execute_check_on_state`](fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs:65), and also asserts balance conservation. -The extension noted in [`docs/fuzzing.md`](docs/fuzzing.md:274) is: +The extension noted in [`docs/fuzzing.md`](docs/fuzzing.md:335) is: > Feed the same block to `SequencerCore` and `indexer_core` and assert identical state roots. @@ -111,7 +111,7 @@ The extension noted in [`docs/fuzzing.md`](docs/fuzzing.md:274) is: | Execution time | Slow (recompile per mutation) | Continuous | | Output | Surviving mutants = assertion gaps | Crash artifacts | -**Decision-maker view**: `cargo-mutants` would **audit the invariant assertions themselves** — revealing if [`assert_invariants()`](fuzz_props/src/invariants.rs:72) has gaps (and it currently does, as [`StateIsolationOnFailure`](fuzz_props/src/invariants.rs:38) and [`ReplayRejection`](fuzz_props/src/invariants.rs:59) are stubs). This is a **complementary quality gate**, not a fuzzing replacement. Low cost (~1 day), highly useful before an external security audit. +**Decision-maker view**: `cargo-mutants` would **audit the invariant assertions themselves** — revealing if [`assert_invariants()`](fuzz_props/src/invariants.rs:90) has gaps. [`StateIsolationOnFailure`](fuzz_props/src/invariants.rs:38) is fully implemented; [`ReplayRejection`](fuzz_props/src/invariants.rs:65) is intentionally a no-op in `InvariantCtx` (enforced directly in `fuzz_state_transition` and `fuzz_replay_prevention`). This is a **complementary quality gate**, not a fuzzing replacement. Low cost (~1 day), highly useful before an external security audit. --- @@ -135,9 +135,9 @@ The current implementation is **well-architected and production-ready** for a pr **Highest-ROI next steps, in priority order:** -1. **Complete the stub invariants** in [`fuzz_props/src/invariants.rs`](fuzz_props/src/invariants.rs:41) — [`StateIsolationOnFailure`](fuzz_props/src/invariants.rs:38) and [`ReplayRejection`](fuzz_props/src/invariants.rs:59) are currently no-ops. This costs less than one day and immediately hardens all existing targets. +1. **Verify and extend the inline invariants** — [`StateIsolationOnFailure`](fuzz_props/src/invariants.rs:38) in [`fuzz_props/src/invariants.rs`](fuzz_props/src/invariants.rs:40) is fully implemented. [`ReplayRejection`](fuzz_props/src/invariants.rs:65) is intentionally a no-op in `InvariantCtx` but enforced directly in `fuzz_state_transition` and `fuzz_replay_prevention`. Add `BalanceConservation` as a registered `ProtocolInvariant` to make it reusable across all targets (currently checked inline only). -2. **Add the sequencer-vs-replayer differential target** — highest new bug-finding value, unique to this protocol's architecture, already identified in [`docs/fuzzing.md`](docs/fuzzing.md:274). +2. **Add the sequencer-vs-replayer differential target** — highest new bug-finding value, unique to this protocol's architecture, already identified in [`docs/fuzzing.md`](docs/fuzzing.md:335). 3. **Add AFL++ as a parallel fuzzing lane** (`just fuzz-afl`) — zero corpus migration cost, discovers different mutation paths through the same targets as libFuzzer. diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 42e6a9a..2778b86 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -65,13 +65,13 @@ just fuzz-regression |--------|---------------|-------------| | `fuzz_transaction_decoding` | Borsh decoding of `NSSATransaction`, `Block`, and `HashableBlockData`; roundtrip re-encoding of successfully decoded transactions | `fuzz/fuzz_targets/fuzz_transaction_decoding.rs` | | `fuzz_stateless_verification` | `transaction_stateless_check()` no-panic on arbitrary bytes; idempotency — a transaction that passes the check must pass it again | `fuzz/fuzz_targets/fuzz_stateless_verification.rs` | -| `fuzz_state_transition` | `execute_check_on_state()` across up to 8 transactions with monotonically-advancing block context; asserts balance isolation on rejection | `fuzz/fuzz_targets/fuzz_state_transition.rs` | -| `fuzz_block_verification` | `block_hash()` no-panic and determinism — recomputing the hash of any fuzz-generated `Block` must never panic and must return the same value on repeated calls | `fuzz/fuzz_targets/fuzz_block_verification.rs` | -| `fuzz_encoding_roundtrip` | `decode(encode(tx)) == Ok(tx)` and `encode(decode(encode(tx))) == encode(tx)` for `PublicTransaction` and `ProgramDeploymentTransaction` | `fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs` | +| `fuzz_state_transition` | `execute_check_on_state()` across up to 8 transactions with fuzz-driven initial state and monotonically-advancing block context; asserts **StateIsolationOnFailure** (balances unchanged on rejection), **BalanceConservation** (total balance unchanged on success), and **ReplayRejection** (nonce consumed on first acceptance) | `fuzz/fuzz_targets/fuzz_state_transition.rs` | +| `fuzz_block_verification` | Three block-hash invariants: **HashRoundTrip** (`HashableBlockData::from(Block)` is lossless), **HashPreimage** (block_id, prev_block_hash, timestamp each individually affect the hash), **TxOrderCommitment** (reversing the transaction list changes the hash) | `fuzz/fuzz_targets/fuzz_block_verification.rs` | +| `fuzz_encoding_roundtrip` | `decode(encode(tx)) == Ok(tx)` and `encode(decode(encode(tx))) == encode(tx)` for `PublicTransaction` and `ProgramDeploymentTransaction`; raw bytes that decode successfully must re-encode identically (canonical encoding) | `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` | A tx accepted in block N must be rejected when replayed in block N+1 (nonce consumed) | `fuzz/fuzz_targets/fuzz_replay_prevention.rs` | -| `fuzz_state_diff_computation` | `ValidatedStateDiff` only modifies accounts declared in `affected_public_account_ids()` | `fuzz/fuzz_targets/fuzz_state_diff_computation.rs` | -| `fuzz_validate_execute_consistency` | `validate_on_state` and `execute_check_on_state` must agree on success/failure and produce identical state changes | `fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs` | +| `fuzz_replay_prevention` | A tx accepted in block N must be rejected when replayed in block N+1 (nonce consumed); fuzz-driven initial state exposes nonce edge cases (nonce 0, `u128::MAX`, zero-balance sender) | `fuzz/fuzz_targets/fuzz_replay_prevention.rs` | +| `fuzz_state_diff_computation` | **Forward containment**: `ValidatedStateDiff` only modifies accounts declared in `affected_public_account_ids()`; **Reverse completeness**: every declared account actually modified by `execute_check_on_state` appears in the diff | `fuzz/fuzz_targets/fuzz_state_diff_computation.rs` | +| `fuzz_validate_execute_consistency` | `validate_on_state` and `execute_check_on_state` must agree on success/failure; diff accuracy (forward + reverse); **BalanceConservation** on success | `fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs` | --- @@ -210,12 +210,28 @@ Open a PR. The `regression` CI job will permanently block re-introduction of thi Shared invariants live in `fuzz_props/src/invariants.rs`. Each invariant implements `ProtocolInvariant` and is automatically run by `assert_invariants()`. -Concrete invariants currently registered: +Concrete invariants currently registered in `assert_invariants()`: -| Invariant | Description | -|-----------|-------------| -| `StateIsolationOnFailure` | Account balances must not change when a transaction is rejected | -| `ReplayRejection` | An accepted transaction must be rejected when replayed (see `fuzz_replay_prevention`) | +| Invariant | Description | Implementation status | +|-----------|-------------|----------------------| +| `StateIsolationOnFailure` | Per-account balance must not change for any tracked account when a transaction is rejected | ✅ Fully implemented in `fuzz_props/src/invariants.rs` | +| `ReplayRejection` | An accepted transaction must be rejected when replayed | ⚠️ Returns `None` from `InvariantCtx` (see note below); enforced directly in `fuzz_state_transition` and `fuzz_replay_prevention` | + +> **Note on `ReplayRejection`:** The check cannot be fully exercised through `InvariantCtx` +> because it requires re-applying the `ValidatedTransaction` returned on `Ok` by +> `execute_check_on_state` — which consumes `self` — to the post-execution state. The +> invariant is enforced in two complementary ways: (1) `fuzz_state_transition.rs` replays +> every accepted transaction in the next block and asserts rejection; (2) the proptest suite +> `replay_rejection_proptest` in `fuzz_props/src/invariants.rs` exercises the same property +> with reproducible structured inputs. + +Additional invariants enforced **inline** in specific targets (not via `ProtocolInvariant`): + +| Invariant | Targets | +|-----------|---------| +| `BalanceConservation` | `fuzz_state_transition`, `fuzz_validate_execute_consistency` | +| `HashRoundTrip` / `HashPreimage` / `TxOrderCommitment` | `fuzz_block_verification` | +| Diff forward containment / reverse completeness | `fuzz_state_diff_computation` | To add a new invariant: @@ -248,15 +264,17 @@ fuzz target parameters for zero-boilerplate structured fuzzing. | `ArbHashableBlockData` | `HashableBlockData` (0–7 `ArbNSSATransaction` entries, random header fields) | | `ArbNSSATransaction` | `NSSATransaction` (`Public` or `ProgramDeployment` variant; `PrivacyPreserving` excluded) | -### `fuzz_props::generators` (proptest strategies + libFuzzer helpers) +### `fuzz_props::generators` (libFuzzer helpers + proptest strategies) | Generator | Covers | |-----------|--------| -| `arbitrary_transaction()` | Best-effort structured `NSSATransaction` from unstructured bytes, falls back to raw Borsh decode | -| `arb_borsh_transaction_bytes()` | Raw Borsh bytes including invalid encodings (IS-2) | -| `arb_native_transfer_tx()` | Valid native-transfer `NSSATransaction` between known genesis accounts | +| `arbitrary_fuzz_state()` | 1–8 fuzz-driven accounts with arbitrary IDs, balances, and private keys; used by `fuzz_state_transition`, `fuzz_replay_prevention`, `fuzz_validate_execute_consistency`, `fuzz_state_diff_computation` | +| `arb_fuzz_native_transfer()` | Correctly-signed native-transfer `NSSATransaction` referencing accounts from an `arbitrary_fuzz_state()` result; gives the fuzzer a path to successful state transitions | +| `arbitrary_transaction()` | Structured `NSSATransaction` (`Public` or `ProgramDeployment`) from unstructured bytes via `ArbNSSATransaction` | +| `arb_borsh_transaction_bytes()` | Raw Borsh bytes including invalid encodings | +| `arb_native_transfer_tx()` | Valid native-transfer `NSSATransaction` between known testnet genesis accounts (proptest strategy) | | `test_accounts()` | Returns `(AccountId, PrivateKey)` pairs from `testnet_initial_state` | -| `arb_hashable_block_data()` | `HashableBlockData` with 0–8 valid native transfers | +| `arb_hashable_block_data()` | `HashableBlockData` with 0–8 valid native transfers (proptest strategy) | | `arb_invalid_account_state_tx()` | Phantom accounts + overflow amounts — expected to be rejected (IS-3) | | `arb_duplicate_tx_sequence()` | Duplicated + re-ordered transaction sequences (IS-4) | | `arb_pathological_sequence()` | Zero-value, self-transfer, max-nonce inputs (IS-5) | diff --git a/presentation_qa_team.md b/presentation_qa_team.md deleted file mode 100644 index b1d48ea..0000000 --- a/presentation_qa_team.md +++ /dev/null @@ -1,509 +0,0 @@ -# LEZ Fuzzing — QA Team Presentation - -> **Project:** `lez-fuzzing` — Automated Fuzz Testing for the Logos Execution Zone (LEZ) -> **Audience:** QA Team -> **Date:** April 2026 - ---- - -## Agenda - -1. [What Is This Project?](#1-what-is-this-project) -2. [Why Fuzzing? (Not Just Unit Tests)](#2-why-fuzzing-not-just-unit-tests) -3. [Architecture Overview](#3-architecture-overview) -4. [What We Are Testing — 9 Fuzz Targets](#4-what-we-are-testing--9-fuzz-targets) -5. [Protocol Invariants — The Safety Net](#5-protocol-invariants--the-safety-net) -6. [Input Generation Strategy](#6-input-generation-strategy) -7. [How to Run Locally](#7-how-to-run-locally) -8. [CI/CD Integration](#8-cicd-integration) -9. [Performance Characteristics](#9-performance-characteristics) -10. [Known Limitations & Future Work](#10-known-limitations--future-work) -11. [Key Takeaways for QA](#11-key-takeaways-for-qa) - ---- - -## 1. What Is This Project? - -`lez-fuzzing` is a **coverage-guided, structured mutation fuzzing system** for the **Logos Execution Zone (LEZ)** blockchain protocol. - -### High-Level Context - -``` -/ -├── logos-execution-zone/ ← Production codebase (LEZ protocol) -│ ├── nssa/ ← Node State & State Accumulator -│ ├── common/ ← Shared types (transactions, blocks) -│ └── key_protocol/ ← Cryptographic primitives -└── lez-fuzzing/ ← This repository (fuzzing harness) - ├── fuzz_props/ ← Reusable: generators + invariants - └── fuzz/ ← Fuzz targets + pre-seeded corpus - └── fuzz_targets/ ← 9 individual fuzz entry points -``` - -### What the Fuzzer Does - -The fuzzer automatically generates **millions of malformed, adversarial, and boundary-case inputs** and feeds them into the protocol. It then checks that: -- The process never **panics or crashes unexpectedly** -- Protocol **invariants** (safety rules) are never violated -- **Encoding/decoding** is lossless and deterministic -- **State integrity** is preserved even on rejected transactions - ---- - -## 2. Why Fuzzing? (Not Just Unit Tests) - -### The Gap Unit Tests Leave - -Unit tests check what engineers **think of** in advance. Fuzzing discovers what engineers **don't think of**. - -| Technique | Finds Known Bugs | Finds Unknown Bugs | Coverage Guidance | Scale | -|---|---|---|---|---| -| Unit tests | ✅ | ❌ | Manual | Hundreds of cases | -| Property tests (proptest) | ✅ | Partial | ❌ None | Thousands of cases | -| **Fuzzing (libFuzzer)** | ✅ | ✅ | ✅ LLVM-driven | **Millions/sec** | - -### Bugs Fuzzing Is Uniquely Good At Finding - -- **Panic on malformed input** — decoder receives garbled bytes → should return `Err`, not crash -- **State leakage on rejection** — a rejected transaction changes account balances (silent corruption) -- **Replay attacks** — a transaction accepted in block N is accepted again in block N+1 -- **Encoding non-determinism** — `encode(decode(encode(x))) ≠ encode(x)` -- **Integer overflow / underflow** in balance arithmetic -- **Phantom account attacks** — transfers from accounts that don't exist in genesis state - -### Why This Matters for a Blockchain Protocol - -On a blockchain, a single invariant violation can lead to: -- **Double-spend** (state leakage on failure or replay acceptance) -- **Consensus split** (non-deterministic hashing) -- **Fund loss** (overflow in balance computation) - ---- - -## 3. Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────┐ -│ lez-fuzzing │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ fuzz_props crate │ │ -│ │ │ │ -│ │ arbitrary_types.rs ← Typed Arbitrary wrappers │ │ -│ │ generators.rs ← proptest + libFuzzer helpers │ │ -│ │ invariants.rs ← ProtocolInvariant trait │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ fuzz/fuzz_targets/ │ │ -│ │ │ │ -│ │ fuzz_transaction_decoding.rs │ │ -│ │ fuzz_stateless_verification.rs │ │ -│ │ fuzz_state_transition.rs (9 targets) │ │ -│ │ 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/corpus/ (pre-seeded) │ │ -│ │ ~150 minimised seed files per target │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↕ path dependencies -┌─────────────────────────────────────────────────────────────┐ -│ ../logos-execution-zone (LEZ) │ -│ nssa · common · key_protocol · token_core … │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Technology Stack - -| Component | Technology | -|---|---| -| Fuzzer engine | **libFuzzer** via `cargo-fuzz` | -| Coverage instrumentation | **LLVM SanitizerCoverage** | -| Structured input generation | **`arbitrary` crate** (typed wrappers) | -| Property-based strategies | **`proptest`** | -| ZK proof layer | **RISC0** (stubbed out with `RISC0_DEV_MODE=1`) | -| Serialization format | **Borsh** (binary object representation) | -| Language | **Rust (nightly toolchain)** | -| CI | **GitHub Actions** | - ---- - -## 4. What We Are Testing — 9 Fuzz Targets - -### Target Map - -| # | Target | What Is Being Tested | Key Invariant | -|---|---|---|---| -| 1 | `fuzz_transaction_decoding` | Borsh decode of all tx/block types | Never panic; roundtrip stable | -| 2 | `fuzz_stateless_verification` | `transaction_stateless_check()` signature validation | No panic on any input | -| 3 | `fuzz_state_transition` | `V03State::transition_from_*()` with 0–8 txs | Balances unchanged on rejection | -| 4 | `fuzz_block_verification` | Block hash integrity + replayer pipeline | `block_hash()` is deterministic | -| 5 | `fuzz_encoding_roundtrip` | `decode(encode(tx)) == Ok(tx)` | Encoding is lossless | -| 6 | `fuzz_signature_verification` | Sign→verify correctness, cross-key soundness | No false positive verifications | -| 7 | `fuzz_replay_prevention` | Tx accepted in block N rejected in block N+1 | Nonce consumed, replay blocked | -| 8 | `fuzz_state_diff_computation` | `ValidatedStateDiff` scope correctness | Only declared accounts modified | -| 9 | `fuzz_validate_execute_consistency` | `validate_on_state` vs `execute_check_on_state` agree | No divergence between validators | - ---- - -### Target Deep-Dives - -#### `fuzz_state_transition` — The Core Safety Target - -This is the most important target from a protocol correctness standpoint. - -``` -Input bytes - ↓ -[Generate up to 8 transactions from fuzz bytes] - ↓ -[Filter: pass only stateless-valid txs] - ↓ -[Apply each tx to V03State] - ↓ -[INVARIANT CHECK on rejection]: - for every account in genesis: - assert balance_before == balance_after -``` - -**What it catches:** Any code path where a **rejected** transaction silently mutates account state — the classic "partial write" class of state corruption bug. - ---- - -#### `fuzz_transaction_decoding` — The Crash-Safety Target - -```rust -fuzz_target!(|data: &[u8]| { - // 1. If it decodes: roundtrip must be stable - if let Ok(tx) = borsh::from_slice::(data) { - let re_encoded = borsh::to_vec(&tx).expect("must succeed"); - // assert stability... - } - - // 2. Block decode: must never panic - let _ = borsh::from_slice::(data); - - // 3. HashableBlockData decode: must never panic - let _ = borsh::from_slice::(data); -}); -``` - -**What it catches:** Panics in the Borsh decoder when receiving malformed bytes (e.g., truncated input, wrong variant tags, overflow in length fields). - ---- - -#### `fuzz_block_verification` — Determinism Target - -```rust -fuzz_target!(|data: &[u8]| { - let Ok(block) = borsh::from_slice::(data) else { return; }; - let hashable = HashableBlockData::from(block.clone()); - - let hash1 = hashable.block_hash(); // first call - let hash2 = hashable.block_hash(); // second call — must match - - assert_eq!(hash1, hash2, "block_hash() is not deterministic"); -}); -``` - -**What it catches:** Non-deterministic hashing — a critical consensus bug where two nodes compute different block hashes for the same block content. - ---- - -## 5. Protocol Invariants — The Safety Net - -The [`fuzz_props/src/invariants.rs`](fuzz_props/src/invariants.rs) module defines a **pluggable invariant framework**. - -### The `ProtocolInvariant` Trait - -```rust -pub trait ProtocolInvariant { - fn name(&self) -> &'static str; - fn check(&self, ctx: &InvariantCtx<'_>) -> Option; -} -``` - -Every invariant receives a **snapshot** of the world before and after a transaction: - -```rust -pub struct InvariantCtx<'a> { - pub state_before: &'a V03State, - pub state_after: &'a V03State, - pub tx: &'a NSSATransaction, - pub result: &'a Result<(), NssaError>, - pub balances_before: BalanceSnapshot, -} -``` - -### Currently Registered Invariants - -| Invariant | Rule | -|---|---| -| `StateIsolationOnFailure` | If a tx is **rejected**, all account balances must be identical before and after | -| `ReplayRejection` | A tx **accepted** in block N must be **rejected** in block N+1 (nonce consumed) | - -### How to Add a New Invariant (for QA team) - -```rust -// 1. Define a zero-size struct -pub struct BalanceConservation; - -// 2. Implement the trait -impl ProtocolInvariant for BalanceConservation { - fn name(&self) -> &'static str { "BalanceConservation" } - fn check(&self, ctx: &InvariantCtx<'_>) -> Option { - let before = ctx.balances_before.total(); - let after = /* sum state_after balances */; - if before != after { - Some(InvariantViolation { - invariant: self.name(), - message: format!("total balance changed: {} → {}", before, after), - }) - } else { - None - } - } -} - -// 3. Register in assert_invariants() -let invariants: &[&dyn ProtocolInvariant] = &[ - &StateIsolationOnFailure, - &ReplayRejection, - &BalanceConservation, // ← new -]; -``` - ---- - -## 6. Input Generation Strategy - -The [`fuzz_props/src/generators.rs`](fuzz_props/src/generators.rs) module provides two generation layers: - -### Layer 1 — Typed `Arbitrary` Wrappers (for libFuzzer) - -These give libFuzzer **structured, valid-looking inputs** instead of random bytes: - -| Wrapper | Generates | -|---|---| -| `ArbNSSATransaction` | Full transaction with realistic fields | -| `ArbPublicTransaction` | Native token transfer | -| `ArbProgramDeploymentTransaction` | Smart contract deploy | -| `ArbPrivateKey` / `ArbPublicKey` | Cryptographic key pairs | -| `ArbSignature` | ECDSA signatures | - -**Why this matters:** Without typed wrappers, libFuzzer would spend most of its time generating bytes that fail Borsh deserialization at the outermost layer — never reaching deeper code paths. - -### Layer 2 — proptest Strategies (richer adversarial scenarios) - -| Strategy | Tests Scenario | -|---|---| -| `arb_native_transfer_tx()` | Valid transfer between known genesis accounts | -| `arb_borsh_transaction_bytes()` | Valid + intentionally invalid Borsh encodings | -| `arb_invalid_account_state_tx()` | Phantom accounts, overflow amounts (IS-3) | -| `arb_duplicate_tx_sequence()` | Duplicated + re-ordered tx sequences (IS-4) | -| `arb_pathological_sequence()` | Zero-value, self-transfer, max-nonce inputs (IS-5) | -| `arb_hashable_block_data()` | Block with 0–8 native transfers | - -### The Hybrid Approach - -``` -libFuzzer mutation engine - ↓ - arbitrary bytes - ↓ - arbitrary_transaction() - ├── 50%: ArbNSSATransaction (structured) - └── 50%: raw Borsh decode (may fail → libFuzzer learns) -``` - -This hybrid means **half the inputs** are structurally valid (reach deep code), and **half** stress the decoder boundary. - ---- - -## 7. How to Run Locally - -### Prerequisites - -```bash -# Nightly Rust is required by cargo-fuzz / libFuzzer -rustup install nightly -rustup component add llvm-tools-preview --toolchain nightly -cargo install cargo-fuzz -``` - -### Repository Setup - -```bash -# Clone both repositories side-by-side: -git clone logos-execution-zone -git clone lez-fuzzing - -# Required directory layout: -# / -# ├── logos-execution-zone/ -# └── lez-fuzzing/ ← work from here -``` - -### Common Commands - -```bash -# Run ALL targets for 30 seconds each (smoke test) -just fuzz - -# Run regression suite (no mutations — just corpus replay) -just fuzz-regression - -# Run a specific target for 2 minutes -RISC0_DEV_MODE=1 cargo fuzz run fuzz_state_transition -- -max_total_time=120 - -# Minimise a crash artifact -just fuzz-tmin fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-abc123 - -# View all available targets -cargo fuzz list -``` - -> ⚠️ **Always use `RISC0_DEV_MODE=1`** — without it, ZK proof generation runs at ~1 proof/sec, making fuzzing impractical. The `just` recipes set this automatically. - -### Adding a New Fuzz Target - -```bash -# Scaffold everything automatically (corpus dir, target file, Cargo.toml, CI matrix) -just new-target my_feature - -# Then implement the target body -$EDITOR fuzz/fuzz_targets/fuzz_my_feature.rs - -# Build and verify -RISC0_DEV_MODE=1 cargo fuzz build fuzz_my_feature -just fuzz-regression -``` - ---- - -## 8. CI/CD Integration - -Three GitHub Actions jobs run on every pull request: - -| Job | Trigger | What It Does | Duration | -|---|---|---|---| -| `smoke-fuzz` | Every PR | Runs each target for 30 seconds | ~5 min | -| `regression` | Every PR | Replays all corpus files (no mutations) | ~2 min | -| `perf-baseline` | Every PR | Measures exec/sec and fails if throughput drops >20% | ~10 min | - -### Failure Workflow - -``` -CI detects crash - ↓ -cargo fuzz tmin → minimised input - ↓ -cargo fuzz fmt → Rust byte literal - ↓ -Add to corpus/ → permanent regression test - ↓ -Open PR → regression job blocks reintroduction forever -``` - ---- - -## 9. Performance Characteristics - -Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`: - -| Target | Throughput | Why | -|---|---|---| -| `fuzz_transaction_decoding` | **~200,000 exec/sec** | Pure decode, no state | -| `fuzz_encoding_roundtrip` | **~150,000 exec/sec** | Decode + encode, no state | -| `fuzz_block_verification` | **~50,000 exec/sec** | Hash computation | -| `fuzz_state_transition` | **~5,000 exec/sec** | Full state machine execution | -| `fuzz_replay_prevention` | **~5,000 exec/sec** | Two state transitions per input | -| `fuzz_validate_execute_consistency` | **~3,000 exec/sec** | Two paths compared | - -### Running on All Cores for Long Sessions - -```bash -RISC0_DEV_MODE=1 cargo fuzz run fuzz_state_transition \ - -- -max_total_time=3600 -jobs=$(nproc) -workers=$(nproc) -``` - ---- - -## 10. Known Limitations & Future Work - -### Current Gaps - -| Gap | Impact | Status | -|---|---|---| -| `StateIsolationOnFailure` invariant is a partial placeholder | Balance corruption may go undetected | Known — needs full account iterator API from LEZ | -| `PrivacyPreservingTransaction` excluded from encoding roundtrip | ZK receipts can't be reconstructed in fuzzing loop | Documented; dedicated slow target planned | -| No version pin between repos | Stale LEZ checkout silently fuzzes wrong code | Known limitation — `just update-lez` is manual | - -### Highest-Value Future Extensions (Priority Order) - -1. **Complete stub invariants** in [`fuzz_props/src/invariants.rs`](fuzz_props/src/invariants.rs) — `StateIsolationOnFailure` and `ReplayRejection` need their full implementations. **Cost: < 1 day. Impact: immediately hardens all 9 targets.** - -2. **Sequencer-vs-Replayer differential target** — Feed the same block to `SequencerCore` and `indexer_core`, assert identical state roots. Catches consensus-splitting divergence. **Cost: 1–2 engineer-weeks. Impact: unique bug class not catchable any other way.** - -3. **Add AFL++ as a parallel fuzzing lane** (`just fuzz-afl`) — Same corpus, different mutation engine, finds different bugs. **Cost: ~1 day. Zero corpus migration.** - -4. **Add `cargo-mutants` before security audit** — Proves the invariant assertions are actually capable of catching the bugs they claim to detect. **Cost: ~1 day.** - -5. **Co-locate fuzz/ into logos-execution-zone/** — Eliminates version drift; standard `cargo fuzz` convention. LEZ CI would run `cargo fuzz build` on every PR. **Cost: ~1 day migration.** - ---- - -## 11. Key Takeaways for QA - -### What the Fuzzer Covers - -✅ **No crash on any byte sequence** — all decoders handle malformed input gracefully -✅ **State integrity on rejection** — failed transactions don't mutate balances -✅ **Replay protection** — spent nonces are permanently rejected -✅ **Encoding determinism** — identical inputs produce identical bytes every time -✅ **Signature soundness** — no false positives, no cross-key verification -✅ **Diff scope** — state changes only affect declared accounts - -### What the Fuzzer Does NOT Cover - -⚠️ **Business logic correctness** — fuzzing checks safety properties, not "is the amount correct" -⚠️ **ZK proof validity** — mocked out; proofs are not generated during fuzzing -⚠️ **Network/consensus layer** — only state machine and encoding layers are fuzzed -⚠️ **`PrivacyPreservingTransaction` encoding roundtrip** — excluded (ZK receipts) - -### QA Team Action Items - -| Action | Who | When | -|---|---|---| -| Run `just fuzz-regression` before merging LEZ changes | Dev / QA | Each LEZ PR | -| Review crash artifacts in `fuzz/artifacts/` when CI fails | QA | On CI failure | -| Add new invariants when new protocol rules are introduced | QA + Dev | Feature additions | -| Run `just update-lez` before long fuzzing sessions | QA | Before overnight runs | -| Add new fuzz targets for new transaction types | QA + Dev | New tx types | - ---- - -## Appendix: Project File Map - -| File / Directory | Purpose | -|---|---| -| [`fuzz_props/src/invariants.rs`](fuzz_props/src/invariants.rs) | `ProtocolInvariant` trait + registered invariants | -| [`fuzz_props/src/generators.rs`](fuzz_props/src/generators.rs) | `proptest` strategies + `Arbitrary` helpers | -| [`fuzz/fuzz_targets/`](fuzz/fuzz_targets/) | 9 fuzz entry points | -| [`fuzz/corpus/`](fuzz/corpus/) | Pre-seeded corpus files (minimised, binary) | -| [`docs/fuzzing.md`](docs/fuzzing.md) | Full operational guide (how-to, commands, CI) | -| [`current_vs_alternative_approach.md`](current_vs_alternative_approach.md) | Comparison with AFL++, proptest-only, formal verification | -| [`colocated_vs_separated.md`](colocated_vs_separated.md) | Architecture decision: separate repo vs. co-located | -| [`fuzz/Cargo.toml`](fuzz/Cargo.toml) | Fuzz workspace manifest (all 9 `[[bin]]` entries) | -| [`Cargo.toml`](Cargo.toml) | Root workspace (fuzz_props + LEZ path dependencies) | - ---- - -*Presentation generated from the `lez-fuzzing` repository. For full operational details, see [`docs/fuzzing.md`](docs/fuzzing.md).* diff --git a/targets_coverage.md b/targets_coverage.md deleted file mode 100644 index 363cf88..0000000 --- a/targets_coverage.md +++ /dev/null @@ -1,20 +0,0 @@ -The project contains 9 distinct fuzz targets covering all four required categories: - -**Transaction decoding / instruction parsing** -- [`fuzz_transaction_decoding.rs`](fuzz/fuzz_targets/fuzz_transaction_decoding.rs:9) — raw `&[u8]` fed to the Borsh parser for `NSSATransaction`, `Block`, and `HashableBlockData`; checks no-panic and encode/decode round-trip identity. -- [`fuzz_encoding_roundtrip.rs`](fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs:15) — structured `Arbitrary` inputs through the custom `to_bytes`/`from_bytes` codec for `PublicTransaction` and `ProgramDeploymentTransaction`; different codec, different types, different invariant. - -**Stateless verification checks** -- [`fuzz_stateless_verification.rs`](fuzz/fuzz_targets/fuzz_stateless_verification.rs:13) — calls the application-level `transaction_stateless_check()` and verifies idempotency (a passing check must pass a second time). -- [`fuzz_signature_verification.rs`](fuzz/fuzz_targets/fuzz_signature_verification.rs:27) — directly exercises the cryptographic primitive layer (`Signature::new` / `is_valid_for`): correctness for the signing key, no-panic on garbage bytes, no-panic on cross-key mismatch. - -**State transition / execution engine** -- [`fuzz_state_transition.rs`](fuzz/fuzz_targets/fuzz_state_transition.rs:43) — multi-transaction sequences with monotonically increasing block context; asserts that a rejected transaction leaves all genesis account balances unchanged. -- [`fuzz_replay_prevention.rs`](fuzz/fuzz_targets/fuzz_replay_prevention.rs:36) — applies the same transaction twice and asserts rejection on the second application (nonce consumed after first acceptance). -- [`fuzz_state_diff_computation.rs`](fuzz/fuzz_targets/fuzz_state_diff_computation.rs:28) — exercises `ValidatedStateDiff::from_public_transaction` and verifies diff containment: only accounts declared in `affected_public_account_ids()` may appear in the diff. -- [`fuzz_validate_execute_consistency.rs`](fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs:42) — runs `validate_on_state` (read-only) and `execute_check_on_state` (mutating) on cloned state and checks bidirectional agreement: same success/failure verdict, and the diff matches the actual mutations in both directions. - -**Block verification / replayer logic** -- [`fuzz_block_verification.rs`](fuzz/fuzz_targets/fuzz_block_verification.rs:15) — the only block-level target; decodes raw bytes as `Block` / `HashableBlockData` and verifies that `block_hash()` never panics and is deterministic. - -Each target differs in entry-point API, input shape, types under test, and the specific invariant asserted, making all nine genuinely distinct. \ No newline at end of file