fix: update documentation

This commit is contained in:
Roman 2026-05-14 10:55:01 +08:00
parent 64e8f335a3
commit 06e5e2e843
No known key found for this signature in database
GPG Key ID: 583BDF43C238B83E
6 changed files with 51 additions and 670 deletions

View File

@ -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` |
---

View File

@ -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 (~301600 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.

View File

@ -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.

View File

@ -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` (07 `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()` | 18 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 08 valid native transfers |
| `arb_hashable_block_data()` | `HashableBlockData` with 08 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) |

View File

@ -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
```
<parent directory>/
├── 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 08 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::<NSSATransaction>(data) {
let re_encoded = borsh::to_vec(&tx).expect("must succeed");
// assert stability...
}
// 2. Block decode: must never panic
let _ = borsh::from_slice::<Block>(data);
// 3. HashableBlockData decode: must never panic
let _ = borsh::from_slice::<HashableBlockData>(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::<Block>(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<InvariantViolation>;
}
```
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<InvariantViolation> {
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 08 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 <LEZ_REPO_URL> logos-execution-zone
git clone <LEZ_FUZZING_REPO_URL> lez-fuzzing
# Required directory layout:
# <parent>/
# ├── 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: 12 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).*

View File

@ -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.