mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-07 11:39:30 +00:00
fix: update documentation
This commit is contained in:
parent
64e8f335a3
commit
06e5e2e843
22
README.md
22
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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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) |
|
||||
|
||||
@ -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 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::<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 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 <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: 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).*
|
||||
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user