From 94d6e9831d59f88e0125e97b4c94eb1024977ae9 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 3 Jul 2026 10:19:28 +0200 Subject: [PATCH] fix: update docs --- README.md | 5 +++-- current_vs_alternative_approach.md | 2 +- docs/fuzzing.md | 10 ++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 935b715fd..70ce8b520 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [Logos Execution Zone (LEZ)](https://github.com/logos-blockchain/logos-execution-zone) protocol.** [![Rust](https://img.shields.io/badge/rust-nightly-orange?logo=rust)](rust-toolchain.toml) -[![Fuzzing](https://img.shields.io/badge/libFuzzer%20%C2%B7%20AFL%2B%2B-22%20targets-blue)](#-fuzz-targets) +[![Fuzzing](https://img.shields.io/badge/libFuzzer%20%C2%B7%20AFL%2B%2B-23%20targets-blue)](#-fuzz-targets) [![Mutation testing](https://img.shields.io/badge/cargo--mutants-enabled-green)](.github/workflows/mutants.yml) [![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE-MIT) @@ -31,7 +31,7 @@ lez-fuzzing/ │ └── generators.rs # Arbitrary / proptest strategies ├── fuzz/ # cargo-fuzz crate (own [workspace] sentinel) │ ├── Cargo.toml -│ ├── fuzz_targets/ # 22 targets total — see table below +│ ├── fuzz_targets/ # 23 targets total — see table below │ │ ├── _template.rs # Template for `just new-target` │ │ └── fuzz_*.rs │ └── corpus/ # Curated seed inputs (one dir per target) @@ -132,6 +132,7 @@ just fuzz-props | 20 | `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: NullifierSetRoundtrip (decode→encode identity for the hand-written impl) | | 21 | `fuzz_privacy_preserving_state_transition` | Path B — `NSSATransaction::PrivacyPreserving` through `execute_check_on_state` with a dev-mode passing proof: reaches commitment/nullifier checks 5–6 + `apply_state_diff`. Asserts no-panic, StateIsolationOnFailure, PrivateStateIsolationOnFailure, CommitmentInsertion, NonceIncrementCorrectness, PostStateApplied, ReplayRejection (balance conservation intentionally not asserted — the fake proof bypasses the circuit guarantee) | | 22 | `fuzz_transaction_ordering_independence` | Transaction ordering-independence on the shielded path: builds a *nullifier-conflicting* pair (two distinct privacy-preserving txs declaring the same nullifier) and applies it in both orders on independent clones of a seeded state, at an identical `(block_id, timestamp)`. Asserts **NoDoubleSpend** (neither ordering accepts both — the shared nullifier is spendable at most once) and **OrderIndependentAcceptance** (the count of accepted txs is the same in both orderings). The nullifier check is enforced by the state machine, not the circuit, so the dev-mode fake proof does not mask it. Requires `RISC0_DEV_MODE=1` | +| 23 | `fuzz_stateful_model_lockstep` | Model-based stateful lockstep: steps an independent hand-written reference model of `(balance, nonce)` per account — reimplementing native-transfer semantics without calling `nssa` — in lockstep with the real `V03State` over a generated schedule of `Transfer`/`AdvanceBlock`/`ReplayLast` commands. Asserts **ModelAcceptanceAgreement** (real accept iff model predicts accept) and **ModelStateAgreement** (every account's real balance & nonce equals the model) after *every* command. The only target with a predictive oracle that pins the exact expected outcome per operation, catching history-dependent acceptance/state drift a self-consistent-but-wrong machine would pass | Each target lives at `fuzz/fuzz_targets/.rs`. diff --git a/current_vs_alternative_approach.md b/current_vs_alternative_approach.md index 9d43c34e6..ce54eab8a 100644 --- a/current_vs_alternative_approach.md +++ b/current_vs_alternative_approach.md @@ -11,7 +11,7 @@ The `lez-fuzzing` repository is a **coverage-guided, structured mutation fuzzing | Rich generators | [`fuzz_props::generators`](fuzz_props/src/generators.rs) adds `proptest` strategies for pathological sequences, phantom-account attacks, overflow amounts, replay sequences | | Protocol invariants | [`fuzz_props::invariants`](fuzz_props/src/invariants.rs) expresses zero-mutation-on-rejection and replay-rejection as reusable `ProtocolInvariant` objects | | ZK-awareness | `RISC0_DEV_MODE=1` stubs out `risc0-zkvm` proofs, enabling ~5 000–200 000 exec/sec depending on target | -| 22 dedicated targets | Covers encoding, signature verification, stateless checks, state transitions, state diffs, replay prevention, validate/execute consistency, block verification, state serialization, witness-set verification, program deployment lifecycle, split-path equivalence, multi-block sequences, sequencer-vs-replayer differential, Merkle-tree invariants, transaction properties, privacy-preserving witness/encoding, nullifier-set round-trips, the privacy-preserving state-transition executor, and transaction ordering-independence (shielded-path nullifier double-spend across orderings). Input-independent invariant checks (genesis contents, getters, system-account guard) are kept as **LEZ unit tests**, not targets — see [`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md) | +| 23 dedicated targets | Covers encoding, signature verification, stateless checks, state transitions, state diffs, replay prevention, validate/execute consistency, block verification, state serialization, witness-set verification, program deployment lifecycle, split-path equivalence, multi-block sequences, sequencer-vs-replayer differential, Merkle-tree invariants, transaction properties, privacy-preserving witness/encoding, nullifier-set round-trips, the privacy-preserving state-transition executor, transaction ordering-independence (shielded-path nullifier double-spend across orderings), and model-based stateful lockstep (an independent reference model of balances/nonces stepped alongside the real state machine, predicting accept/reject per operation). Input-independent invariant checks (genesis contents, getters, system-account guard) are kept as **LEZ unit tests**, not targets — see [`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md) | | CI integration | GitHub Actions libFuzzer (`fuzz.yml`), AFL++ (`fuzz-afl.yml`), and mutation-testing (`mutants.yml`) workflows run on every PR / nightly | | Pre-seeded corpus | Hundreds of minimised seed files in [`fuzz/corpus/`](fuzz/corpus/) ensure regressions are caught instantly | diff --git a/docs/fuzzing.md b/docs/fuzzing.md index de9e74141..72b35c8bc 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -131,6 +131,7 @@ just fuzz-regression | `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: **NullifierSetRoundtrip** (decode→encode identity for the hand-written impl) | `fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs` | | `fuzz_privacy_preserving_state_transition` | Path B — `NSSATransaction::PrivacyPreserving` through `execute_check_on_state` with a dev-mode passing proof, reaching checks 5–6 (`check_commitments_are_new` / `check_nullifiers_are_valid`) and `apply_state_diff`: **No panic**, **StateIsolationOnFailure**, **PrivateStateIsolationOnFailure**, **CommitmentInsertion**, **NonceIncrementCorrectness**, **PostStateApplied**, **ReplayRejection**. Balance conservation is intentionally *not* asserted — the synthesised fake receipt bypasses the circuit guarantee. Requires `RISC0_DEV_MODE=1` | `fuzz/fuzz_targets/fuzz_privacy_preserving_state_transition.rs` | | `fuzz_transaction_ordering_independence` | Ordering-independence for the shielded path — the only target that compares two *orderings* of the same transactions rather than one fixed order. `arb_conflicting_nullifier_pair` builds two distinct privacy-preserving txs declaring the **same** nullifier; they are applied in both orders (`B→C` and `C→B`) on independent clones of a commitment-seeded state, at an identical `(block_id, timestamp)` so order is the only variable. Asserts **NoDoubleSpend** (a shared nullifier is spendable at most once per ordering) and **OrderIndependentAcceptance** (the number of accepted txs is order-invariant). The nullifier guard is a state-machine check, not a circuit check, so the dev-mode fake receipt does not mask a violation. Requires `RISC0_DEV_MODE=1` | `fuzz/fuzz_targets/fuzz_transaction_ordering_independence.rs` | +| `fuzz_stateful_model_lockstep` | Model-based stateful lockstep — the only target with a predictive oracle. Decodes the input into a schedule of `Transfer` / `AdvanceBlock` / `ReplayLast` commands and steps an independent hand-written reference model of `(balance, nonce)` per account (reimplementing native-transfer accept/reject semantics — nonce equality, `checked_sub`, `checked_add` — without calling `nssa`) in lockstep with the real `V03State`. After *every* command asserts **ModelAcceptanceAgreement** (the real state machine accepts a tx iff the model predicts accept) and **ModelStateAgreement** (every fuzz account's real balance & nonce equals the model). Catches history-dependent acceptance/state drift — a replay wrongly accepted, a nonce that drifts across a sequence, a balance that leaks — that a self-consistent-but-wrong machine would pass. Self-transfers are excluded upstream to keep the oracle exact | `fuzz/fuzz_targets/fuzz_stateful_model_lockstep.rs` | --- @@ -388,8 +389,8 @@ The nightly AFL++ CI workflow has two jobs: | Job | Triggers | Matrix | |-----|----------|--------| -| `afl-smoke` | nightly + `workflow_dispatch` | all 22 targets, 60 s each | -| `afl-coverage-aggregate` | nightly, `needs: afl-smoke` | all 22 targets merged into one LLVM HTML report | +| `afl-smoke` | nightly + `workflow_dispatch` | all 23 targets, 60 s each | +| `afl-coverage-aggregate` | nightly, `needs: afl-smoke` | all 23 targets merged into one LLVM HTML report | The smoke job (one matrix leg per target, on `ubuntu-latest`): 1. Builds AFL++ from source, then builds the target with `cargo afl build --no-default-features --features fuzzer-afl` @@ -399,7 +400,7 @@ The smoke job (one matrix leg per target, on `ubuntu-latest`): The coverage-aggregate job: 1. Downloads every smoke leg's findings -2. Rebuilds all 22 targets with `RUSTFLAGS="-C instrument-coverage"` +2. Rebuilds all 23 targets with `RUSTFLAGS="-C instrument-coverage"` 3. Runs all checked-in corpus + AFL queue inputs through each binary 4. Merges every `.profraw` → one `.profdata` → a single combined HTML report via `llvm-cov show` @@ -592,7 +593,7 @@ fuzz target parameters for zero-boilerplate structured fuzzing. | Generator | Covers | |-----------|--------| -| `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` | +| `arbitrary_fuzz_state()` | 1–8 fuzz-driven accounts with arbitrary IDs, balances, and private keys; IDs colliding with a reserved system account (faucet, bridge, or clock) or already seen are skipped, since genesis overwrites those and would make a caller read back the wrong balance (harness false positive); used by `fuzz_state_transition`, `fuzz_replay_prevention`, `fuzz_validate_execute_consistency`, `fuzz_state_diff_computation`, `fuzz_stateful_model_lockstep` | | `arb_fuzz_native_transfer()` | Correctly-signed native-transfer `LeeTransaction` referencing accounts from an `arbitrary_fuzz_state()` result; gives the fuzzer a path to successful state transitions | | `arbitrary_transaction()` | Structured `LeeTransaction` (`Public` or `ProgramDeployment`) from unstructured bytes via `ArbLeeTransaction` | | `arb_borsh_transaction_bytes()` | Raw Borsh bytes including invalid encodings | @@ -634,6 +635,7 @@ Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`: | `fuzz_nullifier_set_roundtrip` | ~100 000 exec/sec *(estimate)* | | `fuzz_privacy_preserving_state_transition` | slow — dev-mode proof synthesis + verification per exec dominates runtime *(estimate)* | | `fuzz_transaction_ordering_independence` | slow — seeds the commitment set, then synthesises proofs and executes the conflicting pair in both orderings per exec *(estimate)* | +| `fuzz_stateful_model_lockstep` | ~1 000 exec/sec — steps up to 32 commands through the real state machine and reference model per exec *(estimate)* | > [!NOTE] > Throughput figures for the five new targets are rough estimates; run `just perf-baseline`