From e0720cbceb3ae50dda62653ccd5e1b931b103911 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 May 2026 20:41:11 +0800 Subject: [PATCH 01/31] test: initial mutants for props and protocol --- .github/workflows/mutants.yml | 216 +++++++++++++++++++++++++++++++++ .gitignore | 7 ++ Cargo.toml | 16 +++ Justfile | 103 ++++++++++++++++ scripts/mutants-corpus-test.sh | 53 ++++++++ 5 files changed, 395 insertions(+) create mode 100644 .github/workflows/mutants.yml create mode 100644 scripts/mutants-corpus-test.sh diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml new file mode 100644 index 00000000..337d92e6 --- /dev/null +++ b/.github/workflows/mutants.yml @@ -0,0 +1,216 @@ +name: Mutation Testing + +# ── When to run ─────────────────────────────────────────────────────────────── +# Plane A (fuzz_props invariants) runs on every PR that touches harness code. +# Plane B (LEZ protocol vs corpus) is slow (minutes per mutant × many mutants) +# so it only runs on a weekly schedule or on manual dispatch. +on: + pull_request: + paths: + - "fuzz_props/**" + - "fuzz/fuzz_targets/**" + - ".github/workflows/mutants.yml" + schedule: + - cron: "0 4 * * 1" # 04:00 UTC every Monday + workflow_dispatch: + +env: + RISC0_DEV_MODE: "1" + CARGO_TERM_COLOR: always + +jobs: + # ── Plane A: mutate fuzz_props (invariant harness) ──────────────────────── + # Oracle: cargo test -p fuzz_props --release + # Fast (~30–120 s total). Blocks PRs if any invariant-check logic is + # under-tested. + mutants-harness: + name: Mutants — fuzz_props invariants + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Checkout logos-execution-zone + uses: ./.github/actions/checkout-lez + + - name: Install stable Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install logos-blockchain-circuits + uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: mutants-harness-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Install cargo-mutants + run: cargo install cargo-mutants --locked + + # workspace.metadata.cargo-mutants in Cargo.toml sets: + # additional_cargo_args = ["--release"] + # exclude_globs = ["fuzz/fuzz_targets/**"] + # timeout_multiplier = 3.0 + - name: Run mutation tests on fuzz_props + run: | + cargo mutants \ + --package fuzz_props \ + --output mutants-harness.out + + - name: Upload mutants report + if: always() + uses: actions/upload-artifact@v4 + with: + name: mutants-harness-report + path: mutants-harness.out/ + + - name: Write GitHub Step Summary + if: always() + run: | + MISSED=$(wc -l < mutants-harness.out/missed.txt 2>/dev/null | tr -d ' ' || echo 0) + CAUGHT=$(wc -l < mutants-harness.out/caught.txt 2>/dev/null | tr -d ' ' || echo 0) + { + echo "## Mutation Testing — \`fuzz_props\` invariants" + echo "" + echo "| Result | Count |" + echo "|--------|-------|" + echo "| ✅ Caught | ${CAUGHT} |" + echo "| ❌ Survived | ${MISSED} |" + echo "" + if [ "${MISSED}" -gt 0 ]; then + echo "### Surviving mutants (invariant-checker gaps)" + echo '```' + cat mutants-harness.out/missed.txt 2>/dev/null || true + echo '```' + echo "" + echo "> Each surviving mutant represents a mutation in the invariant-checking" + echo "> code that \`cargo test -p fuzz_props\` did not detect." + echo "> Add a property-test that specifically exercises that code path." + else + echo "> All mutants caught — invariant-checking logic is fully covered." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Fail if any mutations survived + run: | + if [ -s mutants-harness.out/missed.txt ]; then + echo "ERROR: surviving mutants found in fuzz_props — see artifact and Step Summary" + cat mutants-harness.out/missed.txt + exit 1 + fi + + # ── Plane B: mutate LEZ protocol code, oracle = corpus regression ───────── + # Each mutant: rebuild nssa/common + replay all 15 fuzz corpora (-runs=0). + # Surviving mutants = protocol bugs the committed corpus has never caught. + # Runs on schedule (weekly Monday) or manual workflow_dispatch only. + mutants-protocol: + name: Mutants — LEZ protocol vs corpus + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' + + steps: + - uses: actions/checkout@v4 + + - name: Checkout logos-execution-zone + uses: ./.github/actions/checkout-lez + + # cargo-fuzz requires nightly. + - name: Install Rust nightly toolchain + uses: dtolnay/rust-toolchain@nightly + with: + components: llvm-tools-preview + + - name: Install logos-blockchain-circuits + uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: mutants-protocol-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Install cargo-fuzz and cargo-mutants + run: | + cargo install cargo-fuzz --locked + cargo install cargo-mutants --locked + + - name: Make corpus-regression wrapper executable + run: chmod +x scripts/mutants-corpus-test.sh + + # Build all 15 fuzz targets once before the mutation loop so that each + # mutant only needs to rebuild the mutated crate, not the fuzz harness. + - name: Pre-build fuzz targets + run: | + for target in \ + fuzz_transaction_decoding fuzz_stateless_verification \ + fuzz_state_transition fuzz_block_verification \ + fuzz_encoding_roundtrip fuzz_signature_verification \ + fuzz_replay_prevention fuzz_state_diff_computation \ + fuzz_validate_execute_consistency fuzz_state_serialization \ + fuzz_witness_set_verification fuzz_program_deployment_lifecycle \ + fuzz_apply_state_diff_split_path fuzz_multi_block_state_sequence \ + fuzz_sequencer_vs_replayer; do + cargo fuzz build "${target}" + done + + # cargo-mutants is invoked from the LEZ workspace (sibling directory). + # FUZZ_REPO is exported so the wrapper script can locate the corpus and + # fuzz directory without relying on relative paths. + - name: Run mutation tests against LEZ (nssa + common) + env: + FUZZ_REPO: ${{ github.workspace }} + working-directory: logos-execution-zone + run: | + cargo mutants \ + --package nssa \ + --package common \ + --in-place \ + --test-command "${{ github.workspace }}/scripts/mutants-corpus-test.sh" \ + --output "${{ github.workspace }}/mutants-protocol.out" \ + --timeout-multiplier 5.0 + + - name: Upload mutants report + if: always() + uses: actions/upload-artifact@v4 + with: + name: mutants-protocol-report + path: mutants-protocol.out/ + + - name: Write GitHub Step Summary + if: always() + run: | + MISSED=$(wc -l < mutants-protocol.out/missed.txt 2>/dev/null | tr -d ' ' || echo 0) + CAUGHT=$(wc -l < mutants-protocol.out/caught.txt 2>/dev/null | tr -d ' ' || echo 0) + { + echo "## Mutation Testing — LEZ protocol vs committed corpus" + echo "" + echo "| Result | Count |" + echo "|--------|-------|" + echo "| ✅ Caught by corpus | ${CAUGHT} |" + echo "| ❌ Survived (corpus gap) | ${MISSED} |" + echo "" + if [ "${MISSED}" -gt 0 ]; then + echo "### Surviving mutants (corpus gaps — protocol bugs not yet reached)" + echo '```' + cat mutants-protocol.out/missed.txt 2>/dev/null || true + echo '```' + echo "" + echo "> For each surviving mutant:" + echo "> 1. Run \`cargo fuzz run \` targeting the mutated function." + echo "> 2. Save the crashing input to \`corpus/libfuzz//\`." + echo "> 3. Commit the corpus entry — the next run will show \`CAUGHT\`." + else + echo "> All mutants caught — committed corpus covers all tested mutation points." + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 0a1758b5..48506a63 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,13 @@ fuzz/coverage/ .DS_Store **/.DS_Store +# ── cargo-mutants outputs ───────────────────────────────────────────────────── +# Local mutation-testing reports (caught.txt, missed.txt, etc.) +# Created by `just mutants-harness` and `just mutants-protocol`. +mutants.out/ +mutants-harness.out/ +mutants-protocol.out/ + # ── Misc ────────────────────────────────────────────────────────────────────── # Performance baseline output from `just perf-baseline` or CI perf_baseline.txt diff --git a/Cargo.toml b/Cargo.toml index af287fcc..39d3287c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,3 +84,19 @@ hmac-sha512 = "1.1.7" itertools = "0.14.0" risc0-build = "3.0.5" logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } + +# ── cargo-mutants configuration (Plane A: mutate fuzz_props invariants) ─────── +# Run with --release to match CI timing. +# fuzz/fuzz_targets/** entry-points use fuzz_entry!() macros that are not +# reachable via `cargo test`; mutations there produce false survivors. +[workspace.metadata.cargo-mutants] +additional_cargo_args = ["--release"] +exclude_globs = ["fuzz/fuzz_targets/**"] +# RISC0 release builds are slower than typical crates; give each mutant extra time. +timeout_multiplier = 3.0 +# The workspace uses path dependencies outside its own directory +# (../logos-execution-zone/*). cargo-mutants normally copies the workspace to a +# temp directory, but the copy does not include the sibling LEZ directory, so the +# build fails immediately. --in-place mutates the original source files in-place +# and avoids the copy, letting cargo resolve ../logos-execution-zone as usual. +in_place = true diff --git a/Justfile b/Justfile index 7bfb9be0..7c6563a8 100644 --- a/Justfile +++ b/Justfile @@ -643,6 +643,109 @@ coverage-all ENGINE="all": fi fi +# ── Mutation testing ────────────────────────────────────────────────────────── +# +# Prerequisites (install once): +# cargo install cargo-mutants +# +# Two planes — run them independently: +# +# Plane A (fast, ~1-5 min): mutates fuzz_props invariant logic. +# Oracle: cargo test -p fuzz_props --release +# Run on every PR that touches fuzz_props/ or fuzz/fuzz_targets/. +# +# Plane B (slow, ~hours): mutates LEZ protocol code (nssa, common). +# Oracle: all 15 fuzz targets replayed against their committed corpus. +# Run weekly or manually to find corpus gaps. + +# Plane A — mutation testing of fuzz_props invariant implementations. +# +# Mutates every function in fuzz_props and checks whether `cargo test -p fuzz_props +# --release` catches the mutation. Surviving mutants identify invariant-checker +# logic that the property tests do not fully exercise. +# +# Workspace metadata in Cargo.toml configures --release, exclude_globs, and +# timeout_multiplier automatically; no extra flags are needed here. +# +# Output: mutants.out/ (human-readable report, also printed to stdout) +mutants-harness: + cargo mutants --package fuzz_props + +# Plane B — mutation testing of the LEZ protocol code against the committed corpus. +# +# Mutates nssa and common in the logos-execution-zone sibling workspace and uses +# scripts/mutants-corpus-test.sh as the oracle. The oracle replays all 15 +# committed libFuzzer corpora (cargo fuzz run -runs=0) against each mutant. +# +# A mutant that SURVIVES means there is no corpus input that triggers the +# relevant protocol invariant at that mutation point — a corpus gap worth +# investigating with a longer fuzz run. +# +# Prerequisites: +# - logos-execution-zone cloned at ../logos-execution-zone +# - cargo-fuzz installed (cargo install cargo-fuzz) +# - cargo-mutants installed (cargo install cargo-mutants --locked) +# +# PACKAGES selects which LEZ crates to mutate (space-separated). +# Default covers the two highest-value protocol crates. +# +# Output report: mutants-protocol.out/ in the repository root. +mutants-protocol PACKAGES="nssa common": + #!/bin/bash + set -euo pipefail + REPO_DIR="$(pwd)" + + if [ ! -d "${REPO_DIR}/../logos-execution-zone" ]; then + echo "ERROR: logos-execution-zone not found at ../logos-execution-zone" + exit 1 + fi + LEZ_DIR="$(cd "${REPO_DIR}/../logos-execution-zone" && pwd)" + + # Build --package flags (one per crate name) + PKG_FLAGS=() + for pkg in {{PACKAGES}}; do + PKG_FLAGS+=(--package "$pkg") + done + + echo "=== Plane B: mutating [{{PACKAGES}}] in logos-execution-zone ===" + echo " Oracle: scripts/mutants-corpus-test.sh (corpus regression, -runs=0)" + echo " Report: ${REPO_DIR}/mutants-protocol.out/" + echo "" + + # cargo-mutants must be run from inside the target workspace. + # FUZZ_REPO tells the oracle script where to find the corpus and fuzz/ dir. + # --output puts the report in our repo root so it's easy to browse/commit. + # --in-place is required because LEZ depends on path crates outside its own + # directory (e.g. the Rust standard toolchain); without it cargo-mutants copies + # the workspace to a temp dir where those relative paths would not resolve. + cd "$LEZ_DIR" + FUZZ_REPO="$REPO_DIR" \ + cargo mutants \ + "${PKG_FLAGS[@]}" \ + --in-place \ + --test-command "${REPO_DIR}/scripts/mutants-corpus-test.sh" \ + --output "${REPO_DIR}/mutants-protocol.out" \ + --timeout-multiplier 5.0 + + echo "" + echo "=== Mutation report summary ===" + MISSED_FILE="${REPO_DIR}/mutants-protocol.out/missed.txt" + CAUGHT_FILE="${REPO_DIR}/mutants-protocol.out/caught.txt" + MISSED=$(wc -l < "$MISSED_FILE" 2>/dev/null | tr -d ' ' || echo 0) + CAUGHT=$(wc -l < "$CAUGHT_FILE" 2>/dev/null | tr -d ' ' || echo 0) + echo "Caught: ${CAUGHT} | Survived: ${MISSED}" + echo "" + if [ "${MISSED}" -gt 0 ]; then + echo "Surviving mutants (corpus gaps):" + cat "$MISSED_FILE" || true + echo "" + echo "For each surviving mutant: run 'just fuzz ' targeting the" + echo "mutated function, add the crashing input to corpus/libfuzz//," + echo "then re-run 'just mutants-protocol' to confirm it is now CAUGHT." + else + echo "All mutants caught — corpus covers all tested mutation points." + fi + # ── Housekeeping ────────────────────────────────────────────────────────────── # Remove all Cargo build artefacts (workspace + fuzz sub-crate) diff --git a/scripts/mutants-corpus-test.sh b/scripts/mutants-corpus-test.sh new file mode 100644 index 00000000..1bfdead7 --- /dev/null +++ b/scripts/mutants-corpus-test.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Plane-B mutation-testing oracle. +# +# Called by `cargo mutants --test-command` from *inside* the logos-execution-zone +# workspace directory after each source mutation. Replays the committed +# libFuzzer corpus against every fuzz target (cargo fuzz run -runs=0). +# +# Exit behaviour (used by cargo-mutants to classify each mutant): +# exit 0 → all corpus replays passed → mutant SURVIVED (corpus gap) +# exit ≠0 → at least one replay panicked → mutant CAUGHT (corpus covers it) +# +# Environment variables: +# FUZZ_REPO absolute path to the lez-fuzzing repository root. +# Defaults to the directory one level above this script. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FUZZ_REPO="${FUZZ_REPO:-"$(cd "${SCRIPT_DIR}/.." && pwd)"}" + +CORPUS_ROOT="${FUZZ_REPO}/corpus/libfuzz" +FUZZ_DIR="${FUZZ_REPO}/fuzz" + +targets=( + fuzz_transaction_decoding + fuzz_stateless_verification + fuzz_state_transition + fuzz_block_verification + fuzz_encoding_roundtrip + fuzz_signature_verification + fuzz_replay_prevention + fuzz_state_diff_computation + fuzz_validate_execute_consistency + fuzz_state_serialization + fuzz_witness_set_verification + fuzz_program_deployment_lifecycle + fuzz_apply_state_diff_split_path + fuzz_multi_block_state_sequence + fuzz_sequencer_vs_replayer +) + +for target in "${targets[@]}"; do + corpus="${CORPUS_ROOT}/${target}" + mkdir -p "${corpus}" + + # -runs=0 → replay every file in the corpus directory exactly once, then exit. + # A panic (invariant violation) causes cargo fuzz to exit non-zero, which + # propagates through this script and causes cargo-mutants to mark the mutant + # as CAUGHT. + cargo fuzz run "${target}" \ + --fuzz-dir "${FUZZ_DIR}" \ + "${corpus}" \ + -- -runs=0 +done From 1da53a9566d1fa375387f948b00314b0f7366aa8 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 May 2026 20:50:45 +0800 Subject: [PATCH 02/31] fix: resolve path to LEZ --- .github/workflows/mutants.yml | 1 + Justfile | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index 337d92e6..08d0ee21 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -61,6 +61,7 @@ jobs: run: | cargo mutants \ --package fuzz_props \ + --in-place \ --output mutants-harness.out - name: Upload mutants report diff --git a/Justfile b/Justfile index 7c6563a8..a2ad2aa6 100644 --- a/Justfile +++ b/Justfile @@ -665,11 +665,15 @@ coverage-all ENGINE="all": # logic that the property tests do not fully exercise. # # Workspace metadata in Cargo.toml configures --release, exclude_globs, and -# timeout_multiplier automatically; no extra flags are needed here. +# timeout_multiplier automatically. # -# Output: mutants.out/ (human-readable report, also printed to stdout) +# --in-place is mandatory: fuzz_props depends on LEZ crates via relative path +# (../logos-execution-zone/...) — without it cargo-mutants copies the workspace +# to /tmp and the copy cannot resolve those relative paths. +# +# Output: mutants-harness.out/ (human-readable report also printed to stdout) mutants-harness: - cargo mutants --package fuzz_props + cargo mutants --package fuzz_props --in-place --output mutants-harness.out # Plane B — mutation testing of the LEZ protocol code against the committed corpus. # From ee7b3b0f69357ea3b8e9675fa14c6864364cbb08 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 28 May 2026 21:27:48 +0800 Subject: [PATCH 03/31] fix: mutants-protocol invocation --- .github/workflows/mutants.yml | 42 ++++++++++++++++++++++----------- Justfile | 44 +++++++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index 08d0ee21..1c8e70c9 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -165,21 +165,37 @@ jobs: cargo fuzz build "${target}" done - # cargo-mutants is invoked from the LEZ workspace (sibling directory). - # FUZZ_REPO is exported so the wrapper script can locate the corpus and - # fuzz directory without relying on relative paths. + # cargo-mutants >=24 dropped --test-command; intercept "cargo test" with a + # fake cargo wrapper that runs the corpus oracle instead. cargo-mutants is + # called as a direct binary (not through `cargo`) so the CARGO env var we + # set is respected rather than being overridden by cargo's process launch. - name: Run mutation tests against LEZ (nssa + common) - env: - FUZZ_REPO: ${{ github.workspace }} - working-directory: logos-execution-zone run: | - cargo mutants \ - --package nssa \ - --package common \ - --in-place \ - --test-command "${{ github.workspace }}/scripts/mutants-corpus-test.sh" \ - --output "${{ github.workspace }}/mutants-protocol.out" \ - --timeout-multiplier 5.0 + REAL_CARGO="$(command -v cargo)" + FAKE_CARGO="$(mktemp /tmp/fake-cargo-XXXXXX)" + # Intercept the test *execution* phase only; forward the build phase + # (cargo test --no-run) to the real cargo so mutants are compiled. + # cargo-mutants uses: + # Build phase: cargo test --no-run --verbose --package=... + # Test phase: cargo test --verbose --package=... + printf '#!/bin/bash\n_has_no_run=false\nfor _a in "$@"; do [ "$_a" = "--no-run" ] && _has_no_run=true && break; done\nif [ "${1:-}" = "test" ] && [ "$_has_no_run" = "false" ]; then\n FUZZ_REPO="%s" exec "%s"\nelse\n exec "%s" "$@"\nfi\n' \ + "${{ github.workspace }}" \ + "${{ github.workspace }}/scripts/mutants-corpus-test.sh" \ + "$REAL_CARGO" > "$FAKE_CARGO" + chmod +x "$FAKE_CARGO" + # cargo install places cargo-mutants next to cargo in the same bin dir. + MUTANTS_BIN="$(command -v cargo-mutants 2>/dev/null || echo "$(dirname "$REAL_CARGO")/cargo-mutants")" + cd "${{ github.workspace }}/logos-execution-zone" + # cargo-mutants is a Cargo plugin; when invoked directly (not via + # `cargo mutants`) we must supply "mutants" as argv[1] ourselves. + CARGO="$FAKE_CARGO" \ + "$MUTANTS_BIN" mutants \ + --package nssa \ + --package common \ + --in-place \ + --output "${{ github.workspace }}/mutants-protocol.out" \ + --timeout-multiplier 5.0 + rm -f "$FAKE_CARGO" - name: Upload mutants report if: always() diff --git a/Justfile b/Justfile index a2ad2aa6..1e36971a 100644 --- a/Justfile +++ b/Justfile @@ -722,14 +722,50 @@ mutants-protocol PACKAGES="nssa common": # --in-place is required because LEZ depends on path crates outside its own # directory (e.g. the Rust standard toolchain); without it cargo-mutants copies # the workspace to a temp dir where those relative paths would not resolve. + # + # cargo-mutants >=24 dropped --test-command and only supports --test-tool cargo|nextest. + # Work around: create a fake `cargo` wrapper that intercepts `cargo test` and + # runs the corpus oracle instead; every other sub-command is delegated to the + # real cargo. We call the cargo-mutants binary directly so that cargo's own + # process launch doesn't override the CARGO env var back to the real binary. + REAL_CARGO="$(command -v cargo)" + FAKE_CARGO=$(mktemp /tmp/fake-cargo-XXXXXX) + FAKE_CARGO_LOG=$(mktemp /tmp/fake-cargo-log-XXXXXX.txt) + trap 'rm -f "$FAKE_CARGO" "$FAKE_CARGO_LOG"' EXIT + # The fake cargo intercepts the test *execution* phase only. + # cargo-mutants drives two kinds of "cargo test" invocations: + # Build phase: cargo test --no-run --verbose --package=... (compile only) + # Test phase: cargo test --verbose --package=... (run tests) + # The oracle must only replace the test execution phase; the build phase + # must be forwarded to the real cargo so mutants are actually compiled. + printf '#!/bin/bash\necho "FAKE_CARGO: $*" >> "%s"\n_has_no_run=false\nfor _a in "$@"; do [ "$_a" = "--no-run" ] && _has_no_run=true && break; done\nif [ "${1:-}" = "test" ] && [ "$_has_no_run" = "false" ]; then\n FUZZ_REPO="%s" exec "%s"\nelse\n exec "%s" "$@"\nfi\n' \ + "$FAKE_CARGO_LOG" \ + "$REPO_DIR" \ + "${REPO_DIR}/scripts/mutants-corpus-test.sh" \ + "$REAL_CARGO" > "$FAKE_CARGO" + chmod +x "$FAKE_CARGO" + + # Locate the cargo-mutants binary (installed by `cargo install cargo-mutants`). + MUTANTS_BIN="$(command -v cargo-mutants 2>/dev/null || true)" + if [ -z "$MUTANTS_BIN" ]; then + MUTANTS_BIN="$(dirname "$REAL_CARGO")/cargo-mutants" + fi + if [ ! -x "$MUTANTS_BIN" ]; then + echo "ERROR: cargo-mutants not found. Install with: cargo install cargo-mutants --locked" + exit 1 + fi + + # cargo-mutants is a Cargo plugin. When invoked via `cargo mutants`, Cargo + # automatically prepends "mutants" as argv[1]. When we invoke the binary + # directly (to keep our CARGO env override alive), we must supply it ourselves. cd "$LEZ_DIR" - FUZZ_REPO="$REPO_DIR" \ - cargo mutants \ + CARGO="$FAKE_CARGO" \ + "$MUTANTS_BIN" mutants \ "${PKG_FLAGS[@]}" \ --in-place \ - --test-command "${REPO_DIR}/scripts/mutants-corpus-test.sh" \ --output "${REPO_DIR}/mutants-protocol.out" \ - --timeout-multiplier 5.0 + --timeout-multiplier 5.0 \ + || { echo "--- fake-cargo invocations ---"; cat "$FAKE_CARGO_LOG"; exit 1; } echo "" echo "=== Mutation report summary ===" From 477edb48bbdcb59fcdfb06287973f069b41622bc Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 3 Jun 2026 12:40:24 +0800 Subject: [PATCH 04/31] fix: permission and start directory --- scripts/mutants-corpus-test.sh | 7 +++++++ 1 file changed, 7 insertions(+) mode change 100644 => 100755 scripts/mutants-corpus-test.sh diff --git a/scripts/mutants-corpus-test.sh b/scripts/mutants-corpus-test.sh old mode 100644 new mode 100755 index 1bfdead7..cd95d555 --- a/scripts/mutants-corpus-test.sh +++ b/scripts/mutants-corpus-test.sh @@ -38,6 +38,13 @@ targets=( fuzz_sequencer_vs_replayer ) +# cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.). +# When this script is called by `cargo-mutants` the working directory is the +# LEZ workspace (logos-execution-zone/), whose rust-toolchain.toml pins the +# stable 1.x compiler. Change to the fuzzing repo so that rustup resolves +# the nightly toolchain from lez-fuzzing/rust-toolchain.toml instead. +cd "${FUZZ_REPO}" + for target in "${targets[@]}"; do corpus="${CORPUS_ROOT}/${target}" mkdir -p "${corpus}" From a11f5540406bf819f48ac5508d55da504d6d5d54 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 5 Jun 2026 10:04:26 +0800 Subject: [PATCH 05/31] chore: prevent PR file list display for corpus files --- .gitattributes | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e69de29b From 415e427e889747e401563b006e7738ace3c86cdf Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 5 Jun 2026 10:08:28 +0800 Subject: [PATCH 06/31] test: add crash artifacts for regression testing --- .../fuzz_encoding_roundtrip/regression_0001 | 0 .../regression_0002 | 1 + .../libfuzz/fuzz_state_transition/regression_0003 | 0 .../fuzz_witness_set_verification/regression_0004 | Bin 0 -> 71 bytes .../fuzz_witness_set_verification/regression_0005 | Bin 0 -> 70 bytes 5 files changed, 1 insertion(+) create mode 100644 corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 create mode 100644 corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 create mode 100644 corpus/libfuzz/fuzz_state_transition/regression_0003 create mode 100644 corpus/libfuzz/fuzz_witness_set_verification/regression_0004 create mode 100644 corpus/libfuzz/fuzz_witness_set_verification/regression_0005 diff --git a/corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 b/corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 new file mode 100644 index 00000000..e69de29b diff --git a/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 b/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 new file mode 100644 index 00000000..8c7e5a66 --- /dev/null +++ b/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/corpus/libfuzz/fuzz_state_transition/regression_0003 b/corpus/libfuzz/fuzz_state_transition/regression_0003 new file mode 100644 index 00000000..e69de29b diff --git a/corpus/libfuzz/fuzz_witness_set_verification/regression_0004 b/corpus/libfuzz/fuzz_witness_set_verification/regression_0004 new file mode 100644 index 0000000000000000000000000000000000000000..327f06de5ea4130275f753c8d3b553d59f00379e GIT binary patch literal 71 qcmZ=cG<`Y)2yhsHDG)JzdeU@8I1eJSefsq2BB*jKKp}`;hUoyv(+zR} literal 0 HcmV?d00001 diff --git a/corpus/libfuzz/fuzz_witness_set_verification/regression_0005 b/corpus/libfuzz/fuzz_witness_set_verification/regression_0005 new file mode 100644 index 0000000000000000000000000000000000000000..a5d82fa63689e4d9f5cd8cd0d01c97ff240ee8ce GIT binary patch literal 70 pcmZ=cG<`Y)1A{1tfB<$F1DzHDsuG!=G@TJ5!7_a+SSye>9RLm<2toh= literal 0 HcmV?d00001 From 2adc49136104a6cac704229237e629e9e573edc7 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 5 Jun 2026 11:03:44 +0800 Subject: [PATCH 07/31] fix: recommit with corpus exclusion --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index e69de29b..86ed1824 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1 @@ +corpus/** linguist-generated=true From ccd08aed6f10d204ed7fb2407d87741a7a28f37b Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 5 Jun 2026 11:12:26 +0800 Subject: [PATCH 08/31] chore: synchronize with latest lee introduction --- Cargo.lock | 836 ++++++++++++----- Cargo.toml | 12 +- Justfile | 9 +- README.md | 2 +- docs/fuzzing.md | 14 +- fuzz/Cargo.lock | 840 +++++++++++++----- fuzz/Cargo.toml | 8 +- .../fuzz_apply_state_diff_split_path.rs | 4 +- .../fuzz_sequencer_vs_replayer.rs | 4 +- .../fuzz_state_diff_computation.rs | 6 +- .../fuzz_stateless_verification.rs | 4 +- .../fuzz_targets/fuzz_transaction_decoding.rs | 10 +- .../fuzz_validate_execute_consistency.rs | 4 +- fuzz_props/src/arbitrary_types.rs | 22 +- fuzz_props/src/generators.rs | 38 +- fuzz_props/src/invariants.rs | 20 +- 16 files changed, 1329 insertions(+), 504 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9050d104..51a670b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -134,7 +140,7 @@ dependencies = [ "ark-std 0.4.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "sha2", ] @@ -154,7 +160,7 @@ dependencies = [ "ark-std 0.5.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "fnv", "merlin", "sha2", @@ -220,7 +226,7 @@ dependencies = [ "ark-serialize 0.4.2", "ark-std 0.4.0", "derivative", - "digest", + "digest 0.10.7", "itertools 0.10.5", "num-bigint", "num-traits", @@ -240,7 +246,7 @@ dependencies = [ "ark-serialize 0.5.0", "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "educe", "itertools 0.13.0", "num-bigint", @@ -402,7 +408,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-serialize-derive 0.4.2", "ark-std 0.4.0", - "digest", + "digest 0.10.7", "num-bigint", ] @@ -415,7 +421,7 @@ dependencies = [ "ark-serialize-derive 0.5.0", "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "num-bigint", ] @@ -578,8 +584,6 @@ checksum = "86887daca11d02e0b04f37a9cb81888aae881397fb48ff66494e356aea97554a" dependencies = [ "itertools 0.10.5", "lazy_static", - "rand 0.8.5", - "serde", ] [[package]] @@ -749,7 +753,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", - "gloo-timers", "tokio", ] @@ -855,7 +858,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -882,19 +885,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "bonsai-sdk" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a381a5f681e536070483826412fcfcd6f6637921717c6aa0a3759926899ee9c2" -dependencies = [ - "duplicate", - "maybe-async", - "reqwest", - "serde", - "thiserror 2.0.18", -] - [[package]] name = "borsh" version = "1.6.1" @@ -919,6 +909,14 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bridge_core" +version = "0.1.0" +dependencies = [ + "lee_core", + "serde", +] + [[package]] name = "bs58" version = "0.5.1" @@ -1081,9 +1079,15 @@ name = "clock_core" version = "0.1.0" dependencies = [ "borsh", - "nssa_core", + "lee_core", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cobs" version = "0.3.0" @@ -1103,10 +1107,10 @@ dependencies = [ "borsh", "clock_core", "hex", + "lee", + "lee_core", "log", "logos-blockchain-common-http-client", - "nssa", - "nssa_core", "serde", "serde_with", "sha2", @@ -1140,6 +1144,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-str" version = "0.4.3" @@ -1209,6 +1219,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1268,7 +1287,9 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ + "getrandom 0.4.2", "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -1280,6 +1301,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1289,7 +1319,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "serde", @@ -1400,7 +1430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 1.0.109", ] [[package]] @@ -1409,11 +1439,21 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + [[package]] name = "der-parser" version = "10.0.0" @@ -1520,11 +1560,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", + "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1543,7 +1593,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1586,17 +1636,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" -[[package]] -name = "duplicate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" -dependencies = [ - "heck", - "proc-macro2", - "proc-macro2-diagnostics", -] - [[package]] name = "dyn-clone" version = "1.0.20" @@ -1609,13 +1648,13 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", - "digest", + "der 0.7.10", + "digest 0.10.7", "elliptic-curve", "rfc6979", "serdect", "signature", - "spki", + "spki 0.7.3", ] [[package]] @@ -1624,7 +1663,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "serde", "signature", ] @@ -1676,12 +1715,12 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array 0.14.7", "group", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "serdect", @@ -1755,7 +1794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1789,10 +1828,21 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" name = "faucet_core" version = "0.1.0" dependencies = [ - "nssa_core", + "lee_core", "serde", ] +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "ff" version = "0.13.1" @@ -1809,12 +1859,32 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1995,8 +2065,8 @@ dependencies = [ "arbitrary", "borsh", "common", - "nssa", - "nssa_core", + "lee", + "lee_core", "proptest", "testnet_initial_state", ] @@ -2059,6 +2129,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2073,18 +2144,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "group" version = "0.13.0" @@ -2275,7 +2334,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2363,6 +2422,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ + "ctutils", "typenum", ] @@ -2824,6 +2884,26 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.1", + "rand_core 0.10.1", +] + [[package]] name = "key_protocol" version = "0.1.0" @@ -2836,8 +2916,9 @@ dependencies = [ "hmac-sha512", "itertools 0.14.0", "k256", - "nssa", - "nssa_core", + "lee", + "lee_core", + "ml-kem", "rand 0.8.5", "serde", "sha2", @@ -2882,6 +2963,45 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lee" +version = "0.1.0" +dependencies = [ + "anyhow", + "borsh", + "bridge_core", + "clock_core", + "faucet_core", + "hex", + "k256", + "lee_core", + "log", + "rand 0.8.5", + "risc0-binfmt", + "risc0-build", + "risc0-zkvm", + "serde", + "serde_with", + "sha2", + "thiserror 2.0.18", +] + +[[package]] +name = "lee_core" +version = "0.1.0" +dependencies = [ + "base58", + "borsh", + "bytemuck", + "bytesize", + "chacha20", + "ml-kem", + "risc0-zkvm", + "serde", + "serde_with", + "thiserror 2.0.18", +] + [[package]] name = "libc" version = "0.2.184" @@ -3342,7 +3462,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "logos-blockchain-blend-crypto" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "blake2", "logos-blockchain-groth16", @@ -3350,13 +3470,13 @@ dependencies = [ "logos-blockchain-poseidon2", "logos-blockchain-utils", "rs-merkle-tree", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] name = "logos-blockchain-blend-message" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "blake2", "derivative", @@ -3367,11 +3487,12 @@ dependencies = [ "logos-blockchain-core", "logos-blockchain-groth16", "logos-blockchain-key-management-system-keys", + "logos-blockchain-log-targets", "logos-blockchain-utils", "serde", "serde-big-array", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", "zeroize", ] @@ -3379,7 +3500,7 @@ dependencies = [ [[package]] name = "logos-blockchain-blend-proofs" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ed25519-dalek", "generic-array 1.3.5", @@ -3388,17 +3509,18 @@ dependencies = [ "logos-blockchain-groth16", "logos-blockchain-pol", "logos-blockchain-poq", + "logos-blockchain-poseidon2", "logos-blockchain-utils", "num-bigint", "serde", - "thiserror 1.0.69", + "thiserror 2.0.18", "zeroize", ] [[package]] name = "logos-blockchain-chain-broadcast-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "derivative", @@ -3414,7 +3536,7 @@ dependencies = [ [[package]] name = "logos-blockchain-chain-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", @@ -3436,25 +3558,95 @@ dependencies = [ "serde", "serde_with", "strum", - "thiserror 1.0.69", + "thiserror 2.0.18", + "time", "tokio", "tracing", "tracing-futures", ] +[[package]] +name = "logos-blockchain-circuits-build" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "dirs", + "fd-lock", + "flate2", + "tar", + "ureq", +] + +[[package]] +name = "logos-blockchain-circuits-common" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-poc-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-pol-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-poq-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + [[package]] name = "logos-blockchain-circuits-prover" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-circuits-utils", "tempfile", ] +[[package]] +name = "logos-blockchain-circuits-signature-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-types" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "bytes", + "libc", +] + [[package]] name = "logos-blockchain-circuits-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "dirs", ] @@ -3462,10 +3654,11 @@ dependencies = [ [[package]] name = "logos-blockchain-common-http-client" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "futures", "hex", + "log", "logos-blockchain-chain-broadcast-service", "logos-blockchain-chain-service", "logos-blockchain-core", @@ -3475,14 +3668,15 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", + "tokio-util", "url", ] [[package]] name = "logos-blockchain-core" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "bincode", @@ -3507,20 +3701,21 @@ dependencies = [ "rpds", "serde", "strum", - "thiserror 1.0.69", + "thiserror 2.0.18", + "time", "tracing", ] [[package]] name = "logos-blockchain-cryptarchia-engine" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-pol", "logos-blockchain-utils", "serde", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.18", "time", "tokio", "tracing", @@ -3529,7 +3724,7 @@ dependencies = [ [[package]] name = "logos-blockchain-cryptarchia-sync" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "bytes", "futures", @@ -3540,7 +3735,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -3548,7 +3743,7 @@ dependencies = [ [[package]] name = "logos-blockchain-groth16" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-bn254 0.4.0", "ark-ec 0.4.2", @@ -3566,7 +3761,7 @@ dependencies = [ [[package]] name = "logos-blockchain-http-api-common" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "axum", "logos-blockchain-core", @@ -3574,14 +3769,19 @@ dependencies = [ "logos-blockchain-tracing", "serde", "serde_json", + "serde_urlencoded", "serde_with", + "time", "tracing", + "url", + "utoipa", + "validator", ] [[package]] name = "logos-blockchain-key-management-system-keys" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", @@ -3607,7 +3807,7 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-macros" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "proc-macro2", "quote", @@ -3617,7 +3817,7 @@ dependencies = [ [[package]] name = "logos-blockchain-ledger" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "derivative", "logos-blockchain-blend-crypto", @@ -3636,14 +3836,14 @@ dependencies = [ "rpds", "serde", "serde_arrays", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", ] [[package]] name = "logos-blockchain-libp2p" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "backon", @@ -3654,6 +3854,7 @@ dependencies = [ "igd-next 0.16.2", "libp2p", "logos-blockchain-cryptarchia-sync", + "logos-blockchain-log-targets", "logos-blockchain-utils", "multiaddr", "natpmp", @@ -3662,16 +3863,34 @@ dependencies = [ "rand 0.8.5", "serde", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", "zerocopy", ] +[[package]] +name = "logos-blockchain-log-targets" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" +dependencies = [ + "logos-blockchain-log-targets-macros", +] + +[[package]] +name = "logos-blockchain-log-targets-macros" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "logos-blockchain-mmr" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", @@ -3684,13 +3903,14 @@ dependencies = [ [[package]] name = "logos-blockchain-network-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", "logos-blockchain-core", "logos-blockchain-cryptarchia-sync", "logos-blockchain-libp2p", + "logos-blockchain-log-targets", "logos-blockchain-tracing", "overwatch", "rand 0.8.5", @@ -3704,48 +3924,52 @@ dependencies = [ [[package]] name = "logos-blockchain-poc" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "logos-blockchain-circuits-poc-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", - "logos-blockchain-witness-generator", + "logos-blockchain-proofs-error", "num-bigint", "serde", "serde_json", - "thiserror 2.0.18", "tracing", ] [[package]] name = "logos-blockchain-pol" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "astro-float", + "logos-blockchain-circuits-pol-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", + "logos-blockchain-proofs-error", "logos-blockchain-utils", - "logos-blockchain-witness-generator", "num-bigint", "num-traits", "serde", "serde_json", - "thiserror 2.0.18", "tracing", ] [[package]] name = "logos-blockchain-poq" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "logos-blockchain-circuits-poq-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-pol", - "logos-blockchain-witness-generator", + "logos-blockchain-proofs-error", "num-bigint", "serde", "serde_json", @@ -3756,7 +3980,7 @@ dependencies = [ [[package]] name = "logos-blockchain-poseidon2" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-bn254 0.4.0", "ark-ff 0.4.2", @@ -3764,10 +3988,21 @@ dependencies = [ "num-bigint", ] +[[package]] +name = "logos-blockchain-proofs-error" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" +dependencies = [ + "logos-blockchain-circuits-types", + "logos-blockchain-groth16", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "logos-blockchain-services-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", @@ -3775,24 +4010,25 @@ dependencies = [ "overwatch", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", ] [[package]] name = "logos-blockchain-storage-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", "futures", "logos-blockchain-core", "logos-blockchain-cryptarchia-engine", + "logos-blockchain-log-targets", "logos-blockchain-tracing", "overwatch", "serde", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -3800,12 +4036,13 @@ dependencies = [ [[package]] name = "logos-blockchain-time-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", "log", "logos-blockchain-cryptarchia-engine", + "logos-blockchain-log-targets", "logos-blockchain-tracing", "logos-blockchain-utils", "overwatch", @@ -3822,8 +4059,10 @@ dependencies = [ [[package]] name = "logos-blockchain-tracing" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "flate2", + "logos-blockchain-log-targets", "opentelemetry", "opentelemetry-appender-tracing", "opentelemetry-http", @@ -3846,7 +4085,7 @@ dependencies = [ [[package]] name = "logos-blockchain-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "blake2", @@ -3857,13 +4096,15 @@ dependencies = [ "rand 0.8.5", "serde", "serde_with", + "serde_yaml", + "thiserror 2.0.18", "time", ] [[package]] name = "logos-blockchain-utxotree" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", @@ -3871,27 +4112,21 @@ dependencies = [ "num-bigint", "rpds", "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "logos-blockchain-witness-generator" -version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" -dependencies = [ - "tempfile", + "thiserror 2.0.18", ] [[package]] name = "logos-blockchain-zksign" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-signature-sys", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-poseidon2", - "logos-blockchain-witness-generator", + "logos-blockchain-proofs-error", "num-bigint", "serde", "serde_json", @@ -3965,17 +4200,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "maybe-async" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "memchr" version = "2.8.0" @@ -3989,7 +4213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", - "keccak", + "keccak 0.1.6", "rand_core 0.6.4", "zeroize", ] @@ -4021,6 +4245,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.0" @@ -4032,6 +4266,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha3", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + [[package]] name = "moka" version = "0.12.15" @@ -4228,10 +4487,10 @@ dependencies = [ "ark-ec 0.4.2", "ark-ff 0.4.2", "ark-serialize 0.4.2", - "digest", + "digest 0.10.7", "generic-array 0.14.7", "hex", - "keccak", + "keccak 0.1.6", "log", "rand 0.8.5", "zeroize", @@ -4274,51 +4533,13 @@ dependencies = [ "memchr", ] -[[package]] -name = "nssa" -version = "0.1.0" -dependencies = [ - "anyhow", - "borsh", - "clock_core", - "faucet_core", - "hex", - "k256", - "log", - "nssa_core", - "rand 0.8.5", - "risc0-binfmt", - "risc0-build", - "risc0-zkvm", - "serde", - "serde_with", - "sha2", - "thiserror 2.0.18", -] - -[[package]] -name = "nssa_core" -version = "0.1.0" -dependencies = [ - "base58", - "borsh", - "bytemuck", - "bytesize", - "chacha20", - "k256", - "risc0-zkvm", - "serde", - "serde_with", - "thiserror 2.0.18", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4651,9 +4872,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -4662,8 +4883,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", ] [[package]] @@ -4753,6 +4984,30 @@ dependencies = [ "toml_edit 0.25.11+spec-1.1.0", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -4784,18 +5039,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "version_check", -] - [[package]] name = "prometheus-client" version = "0.22.3" @@ -5057,6 +5300,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -5156,7 +5405,6 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", - "futures-channel", "futures-core", "futures-util", "h2", @@ -5369,7 +5617,7 @@ dependencies = [ "borsh", "bytemuck", "cfg-if", - "digest", + "digest 0.10.7", "hex", "hex-literal", "metal", @@ -5391,7 +5639,6 @@ checksum = "22b7eafb5d85be59cbd9da83f662cf47d834f1b836e14f675d1530b12c666867" dependencies = [ "anyhow", "bincode", - "bonsai-sdk", "borsh", "bytemuck", "bytes", @@ -5480,16 +5727,16 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "signature", - "spki", + "spki 0.7.3", "subtle", "zeroize", ] @@ -5568,7 +5815,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5577,6 +5824,7 @@ version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -5697,9 +5945,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.10", "generic-array 0.14.7", - "pkcs8", + "pkcs8 0.10.2", "serdect", "subtle", "zeroize", @@ -5839,6 +6087,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serdect" version = "0.2.0" @@ -5857,7 +6118,17 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", ] [[package]] @@ -5891,10 +6162,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.12" @@ -5940,7 +6217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5956,7 +6233,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", ] [[package]] @@ -6014,6 +6301,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -6094,6 +6387,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -6104,7 +6408,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6113,8 +6417,8 @@ version = "0.1.0" dependencies = [ "common", "key_protocol", - "nssa", - "nssa_core", + "lee", + "lee_core", "serde", ] @@ -6466,11 +6770,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", + "symlink", "thiserror 2.0.18", "time", "tracing-subscriber 0.3.23", @@ -6683,6 +6988,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "unsigned-varint" version = "0.7.2" @@ -6701,6 +7012,35 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http 1.4.0", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -6714,12 +7054,42 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utoipa" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "uuid" version = "1.23.1" @@ -6731,6 +7101,36 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling 0.20.11", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "valuable" version = "0.1.1" @@ -7362,6 +7762,16 @@ dependencies = [ "time", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xml-rs" version = "0.8.28" diff --git a/Cargo.toml b/Cargo.toml index 39d3287c..f6b0861f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,11 +52,13 @@ unsafe_code = "deny" [workspace.dependencies] # ── LEZ crates — expects logos-execution-zone/ to be cloned at ../logos-execution-zone ── -nssa = { path = "../logos-execution-zone/nssa" } -nssa_core = { path = "../logos-execution-zone/nssa/core" } -common = { path = "../logos-execution-zone/common" } -key_protocol = { path = "../logos-execution-zone/key_protocol" } -testnet_initial_state = { path = "../logos-execution-zone/testnet_initial_state" } +# LEZ reorganised its directory layout; the package= key keeps the old dependency +# alias so that fuzz_props source code (use nssa::...) compiles unchanged. +nssa = { path = "../logos-execution-zone/lee/state_machine", package = "lee" } +nssa_core = { path = "../logos-execution-zone/lee/state_machine/core", package = "lee_core" } +common = { path = "../logos-execution-zone/lez/common" } +key_protocol = { path = "../logos-execution-zone/lee/key_protocol" } +testnet_initial_state = { path = "../logos-execution-zone/lez/testnet_initial_state" } token_core = { path = "../logos-execution-zone/programs/token/core" } test_program_methods = { path = "../logos-execution-zone/test_program_methods" } diff --git a/Justfile b/Justfile index 1e36971a..e552881a 100644 --- a/Justfile +++ b/Justfile @@ -788,10 +788,13 @@ mutants-protocol PACKAGES="nssa common": # ── Housekeeping ────────────────────────────────────────────────────────────── -# Remove all Cargo build artefacts (workspace + fuzz sub-crate) +# Remove all Cargo build artefacts (workspace + fuzz sub-crate + logos-execution-zone) +# Each command is prefixed with `-` so that a missing sibling workspace (LEZ not cloned) +# does not abort the recipe — cargo clean still removes whatever targets are present. clean: - cargo clean - cargo clean --manifest-path fuzz/Cargo.toml + -cargo clean + -cargo clean --manifest-path fuzz/Cargo.toml + -cargo clean --manifest-path ../logos-execution-zone/Cargo.toml # Remove libFuzzer crash/timeout artifacts for all targets (corpus is kept) clean-artifacts: diff --git a/README.md b/README.md index e95c320d..082fa38c 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ just fuzz-props | Target | Protocol layer | Entry point | |--------|---------------|-------------| -| `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_transaction_decoding` | Borsh decoding of all tx/block types (`LeeTransaction`, `Block`, `HashableBlockData`) with roundtrip re-encoding | `fuzz/fuzz_targets/fuzz_transaction_decoding.rs` | | `fuzz_stateless_verification` | `transaction_stateless_check()` no-panic + idempotency | `fuzz/fuzz_targets/fuzz_stateless_verification.rs` | | `fuzz_state_transition` | `V03State` transition: StateIsolationOnFailure + BalanceConservation + ReplayRejection invariants across up to 8 txs with fuzz-driven state | `fuzz/fuzz_targets/fuzz_state_transition.rs` | | `fuzz_block_verification` | Block hash integrity: HashRoundTrip · HashPreimage completeness (block_id/prev_hash/timestamp) · TxOrderCommitment | `fuzz/fuzz_targets/fuzz_block_verification.rs` | diff --git a/docs/fuzzing.md b/docs/fuzzing.md index c65bf7f5..3f8133b8 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -103,7 +103,7 @@ just fuzz-regression | Target | What it fuzzes | Entry point | |--------|---------------|-------------| -| `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_transaction_decoding` | Borsh decoding of `LeeTransaction`, `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 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` | @@ -558,19 +558,19 @@ fuzz target parameters for zero-boilerplate structured fuzzing. | `ArbWitnessSet` | `WitnessSet` (0–3 `(Signature, PublicKey)` pairs; mixes valid and invalid) | | `ArbPublicTransaction` | `PublicTransaction` (composed from `ArbPubTxMessage` + `ArbWitnessSet`) | | `ArbProgramDeploymentTransaction` | `ProgramDeploymentTransaction` (arbitrary bytecode) | -| `ArbHashableBlockData` | `HashableBlockData` (0–7 `ArbNSSATransaction` entries, random header fields) | -| `ArbNSSATransaction` | `NSSATransaction` (`Public` or `ProgramDeployment` variant; `PrivacyPreserving` excluded) | +| `ArbHashableBlockData` | `HashableBlockData` (0–7 `ArbLeeTransaction` entries, random header fields) | +| `ArbLeeTransaction` | `LeeTransaction` (`Public` or `ProgramDeployment` variant; `PrivacyPreserving` excluded) | ### `fuzz_props::generators` (libFuzzer helpers + proptest strategies) | 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` | -| `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_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 | -| `signer_account_ids()` | Extracts `AccountId`s of all signers from an `NSSATransaction`'s witness set; used to derive signer IDs before `apply_state_diff` consumes the diff | -| `arb_native_transfer_tx()` | Valid native-transfer `NSSATransaction` between known testnet genesis accounts (proptest strategy) | +| `signer_account_ids()` | Extracts `AccountId`s of all signers from an `LeeTransaction`'s witness set; used to derive signer IDs before `apply_state_diff` consumes the diff | +| `arb_native_transfer_tx()` | Valid native-transfer `LeeTransaction` 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 (proptest strategy) | | `arb_invalid_account_state_tx()` | Phantom accounts + overflow amounts — expected to be rejected (IS-3) | diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 5ad1b08e..413470e0 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -146,7 +152,7 @@ dependencies = [ "ark-std 0.4.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "sha2", ] @@ -166,7 +172,7 @@ dependencies = [ "ark-std 0.5.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "fnv", "merlin", "sha2", @@ -232,7 +238,7 @@ dependencies = [ "ark-serialize 0.4.2", "ark-std 0.4.0", "derivative", - "digest", + "digest 0.10.7", "itertools 0.10.5", "num-bigint", "num-traits", @@ -252,7 +258,7 @@ dependencies = [ "ark-serialize 0.5.0", "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "educe", "itertools 0.13.0", "num-bigint", @@ -414,7 +420,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-serialize-derive 0.4.2", "ark-std 0.4.0", - "digest", + "digest 0.10.7", "num-bigint", ] @@ -427,7 +433,7 @@ dependencies = [ "ark-serialize-derive 0.5.0", "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "num-bigint", ] @@ -590,8 +596,6 @@ checksum = "86887daca11d02e0b04f37a9cb81888aae881397fb48ff66494e356aea97554a" dependencies = [ "itertools 0.10.5", "lazy_static", - "rand 0.8.5", - "serde", ] [[package]] @@ -761,7 +765,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", - "gloo-timers", "tokio", ] @@ -867,7 +870,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -894,19 +897,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "bonsai-sdk" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a381a5f681e536070483826412fcfcd6f6637921717c6aa0a3759926899ee9c2" -dependencies = [ - "duplicate", - "maybe-async", - "reqwest", - "serde", - "thiserror 2.0.18", -] - [[package]] name = "borsh" version = "1.6.1" @@ -931,6 +921,14 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bridge_core" +version = "0.1.0" +dependencies = [ + "lee_core", + "serde", +] + [[package]] name = "bs58" version = "0.5.1" @@ -1095,9 +1093,15 @@ name = "clock_core" version = "0.1.0" dependencies = [ "borsh", - "nssa_core", + "lee_core", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cobs" version = "0.3.0" @@ -1117,10 +1121,10 @@ dependencies = [ "borsh", "clock_core", "hex", + "lee", + "lee_core", "log", "logos-blockchain-common-http-client", - "nssa", - "nssa_core", "serde", "serde_with", "sha2", @@ -1154,6 +1158,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-str" version = "0.4.3" @@ -1223,6 +1233,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1282,7 +1301,9 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ + "getrandom 0.4.2", "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -1294,6 +1315,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1303,7 +1333,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "serde", @@ -1414,7 +1444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 1.0.109", ] [[package]] @@ -1423,11 +1453,21 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + [[package]] name = "der-parser" version = "10.0.0" @@ -1534,11 +1574,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", + "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1557,7 +1607,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1600,17 +1650,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" -[[package]] -name = "duplicate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" -dependencies = [ - "heck", - "proc-macro2", - "proc-macro2-diagnostics", -] - [[package]] name = "dyn-clone" version = "1.0.20" @@ -1623,13 +1662,13 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", - "digest", + "der 0.7.10", + "digest 0.10.7", "elliptic-curve", "rfc6979", "serdect", "signature", - "spki", + "spki 0.7.3", ] [[package]] @@ -1638,7 +1677,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "serde", "signature", ] @@ -1690,12 +1729,12 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array 0.14.7", "group", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "serdect", @@ -1769,7 +1808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1803,10 +1842,21 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" name = "faucet_core" version = "0.1.0" dependencies = [ - "nssa_core", + "lee_core", "serde", ] +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "ff" version = "0.13.1" @@ -1823,12 +1873,32 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2011,9 +2081,9 @@ dependencies = [ "borsh", "common", "fuzz_props", + "lee", + "lee_core", "libfuzzer-sys", - "nssa", - "nssa_core", "testnet_initial_state", ] @@ -2024,8 +2094,8 @@ dependencies = [ "arbitrary", "borsh", "common", - "nssa", - "nssa_core", + "lee", + "lee_core", "proptest", "testnet_initial_state", ] @@ -2088,6 +2158,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2102,18 +2173,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "group" version = "0.13.0" @@ -2304,7 +2363,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2401,6 +2460,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ + "ctutils", "typenum", ] @@ -2872,6 +2932,26 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.1", + "rand_core 0.10.1", +] + [[package]] name = "key_protocol" version = "0.1.0" @@ -2884,8 +2964,9 @@ dependencies = [ "hmac-sha512", "itertools 0.14.0", "k256", - "nssa", - "nssa_core", + "lee", + "lee_core", + "ml-kem", "rand 0.8.5", "serde", "sha2", @@ -2930,6 +3011,45 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lee" +version = "0.1.0" +dependencies = [ + "anyhow", + "borsh", + "bridge_core", + "clock_core", + "faucet_core", + "hex", + "k256", + "lee_core", + "log", + "rand 0.8.5", + "risc0-binfmt", + "risc0-build", + "risc0-zkvm", + "serde", + "serde_with", + "sha2", + "thiserror 2.0.18", +] + +[[package]] +name = "lee_core" +version = "0.1.0" +dependencies = [ + "base58", + "borsh", + "bytemuck", + "bytesize", + "chacha20", + "ml-kem", + "risc0-zkvm", + "serde", + "serde_with", + "thiserror 2.0.18", +] + [[package]] name = "libc" version = "0.2.184" @@ -3400,7 +3520,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "logos-blockchain-blend-crypto" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "blake2", "logos-blockchain-groth16", @@ -3408,13 +3528,13 @@ dependencies = [ "logos-blockchain-poseidon2", "logos-blockchain-utils", "rs-merkle-tree", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] name = "logos-blockchain-blend-message" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "blake2", "derivative", @@ -3425,11 +3545,12 @@ dependencies = [ "logos-blockchain-core", "logos-blockchain-groth16", "logos-blockchain-key-management-system-keys", + "logos-blockchain-log-targets", "logos-blockchain-utils", "serde", "serde-big-array", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", "zeroize", ] @@ -3437,7 +3558,7 @@ dependencies = [ [[package]] name = "logos-blockchain-blend-proofs" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ed25519-dalek", "generic-array 1.3.5", @@ -3446,17 +3567,18 @@ dependencies = [ "logos-blockchain-groth16", "logos-blockchain-pol", "logos-blockchain-poq", + "logos-blockchain-poseidon2", "logos-blockchain-utils", "num-bigint", "serde", - "thiserror 1.0.69", + "thiserror 2.0.18", "zeroize", ] [[package]] name = "logos-blockchain-chain-broadcast-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "derivative", @@ -3472,7 +3594,7 @@ dependencies = [ [[package]] name = "logos-blockchain-chain-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", @@ -3494,25 +3616,95 @@ dependencies = [ "serde", "serde_with", "strum", - "thiserror 1.0.69", + "thiserror 2.0.18", + "time", "tokio", "tracing", "tracing-futures", ] +[[package]] +name = "logos-blockchain-circuits-build" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "dirs", + "fd-lock", + "flate2", + "tar", + "ureq", +] + +[[package]] +name = "logos-blockchain-circuits-common" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-poc-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-pol-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-poq-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + [[package]] name = "logos-blockchain-circuits-prover" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-circuits-utils", "tempfile", ] +[[package]] +name = "logos-blockchain-circuits-signature-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-types" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "bytes", + "libc", +] + [[package]] name = "logos-blockchain-circuits-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "dirs", ] @@ -3520,10 +3712,11 @@ dependencies = [ [[package]] name = "logos-blockchain-common-http-client" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "futures", "hex", + "log", "logos-blockchain-chain-broadcast-service", "logos-blockchain-chain-service", "logos-blockchain-core", @@ -3533,14 +3726,15 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", + "tokio-util", "url", ] [[package]] name = "logos-blockchain-core" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "bincode", @@ -3565,20 +3759,21 @@ dependencies = [ "rpds", "serde", "strum", - "thiserror 1.0.69", + "thiserror 2.0.18", + "time", "tracing", ] [[package]] name = "logos-blockchain-cryptarchia-engine" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-pol", "logos-blockchain-utils", "serde", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.18", "time", "tokio", "tracing", @@ -3587,7 +3782,7 @@ dependencies = [ [[package]] name = "logos-blockchain-cryptarchia-sync" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "bytes", "futures", @@ -3598,7 +3793,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -3606,7 +3801,7 @@ dependencies = [ [[package]] name = "logos-blockchain-groth16" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-bn254 0.4.0", "ark-ec 0.4.2", @@ -3624,7 +3819,7 @@ dependencies = [ [[package]] name = "logos-blockchain-http-api-common" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "axum", "logos-blockchain-core", @@ -3632,14 +3827,19 @@ dependencies = [ "logos-blockchain-tracing", "serde", "serde_json", + "serde_urlencoded", "serde_with", + "time", "tracing", + "url", + "utoipa", + "validator", ] [[package]] name = "logos-blockchain-key-management-system-keys" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", @@ -3665,7 +3865,7 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-macros" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "proc-macro2", "quote", @@ -3675,7 +3875,7 @@ dependencies = [ [[package]] name = "logos-blockchain-ledger" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "derivative", "logos-blockchain-blend-crypto", @@ -3694,14 +3894,14 @@ dependencies = [ "rpds", "serde", "serde_arrays", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", ] [[package]] name = "logos-blockchain-libp2p" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "backon", @@ -3712,6 +3912,7 @@ dependencies = [ "igd-next 0.16.2", "libp2p", "logos-blockchain-cryptarchia-sync", + "logos-blockchain-log-targets", "logos-blockchain-utils", "multiaddr", "natpmp", @@ -3720,16 +3921,34 @@ dependencies = [ "rand 0.8.5", "serde", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", "zerocopy", ] +[[package]] +name = "logos-blockchain-log-targets" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" +dependencies = [ + "logos-blockchain-log-targets-macros", +] + +[[package]] +name = "logos-blockchain-log-targets-macros" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "logos-blockchain-mmr" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", @@ -3742,13 +3961,14 @@ dependencies = [ [[package]] name = "logos-blockchain-network-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", "logos-blockchain-core", "logos-blockchain-cryptarchia-sync", "logos-blockchain-libp2p", + "logos-blockchain-log-targets", "logos-blockchain-tracing", "overwatch", "rand 0.8.5", @@ -3762,48 +3982,52 @@ dependencies = [ [[package]] name = "logos-blockchain-poc" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "logos-blockchain-circuits-poc-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", - "logos-blockchain-witness-generator", + "logos-blockchain-proofs-error", "num-bigint", "serde", "serde_json", - "thiserror 2.0.18", "tracing", ] [[package]] name = "logos-blockchain-pol" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "astro-float", + "logos-blockchain-circuits-pol-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", + "logos-blockchain-proofs-error", "logos-blockchain-utils", - "logos-blockchain-witness-generator", "num-bigint", "num-traits", "serde", "serde_json", - "thiserror 2.0.18", "tracing", ] [[package]] name = "logos-blockchain-poq" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "logos-blockchain-circuits-poq-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-pol", - "logos-blockchain-witness-generator", + "logos-blockchain-proofs-error", "num-bigint", "serde", "serde_json", @@ -3814,7 +4038,7 @@ dependencies = [ [[package]] name = "logos-blockchain-poseidon2" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-bn254 0.4.0", "ark-ff 0.4.2", @@ -3822,10 +4046,21 @@ dependencies = [ "num-bigint", ] +[[package]] +name = "logos-blockchain-proofs-error" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" +dependencies = [ + "logos-blockchain-circuits-types", + "logos-blockchain-groth16", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "logos-blockchain-services-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", @@ -3833,24 +4068,25 @@ dependencies = [ "overwatch", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", ] [[package]] name = "logos-blockchain-storage-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", "futures", "logos-blockchain-core", "logos-blockchain-cryptarchia-engine", + "logos-blockchain-log-targets", "logos-blockchain-tracing", "overwatch", "serde", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -3858,12 +4094,13 @@ dependencies = [ [[package]] name = "logos-blockchain-time-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", "log", "logos-blockchain-cryptarchia-engine", + "logos-blockchain-log-targets", "logos-blockchain-tracing", "logos-blockchain-utils", "overwatch", @@ -3880,8 +4117,10 @@ dependencies = [ [[package]] name = "logos-blockchain-tracing" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "flate2", + "logos-blockchain-log-targets", "opentelemetry", "opentelemetry-appender-tracing", "opentelemetry-http", @@ -3904,7 +4143,7 @@ dependencies = [ [[package]] name = "logos-blockchain-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "blake2", @@ -3915,13 +4154,15 @@ dependencies = [ "rand 0.8.5", "serde", "serde_with", + "serde_yaml", + "thiserror 2.0.18", "time", ] [[package]] name = "logos-blockchain-utxotree" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", @@ -3929,27 +4170,21 @@ dependencies = [ "num-bigint", "rpds", "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "logos-blockchain-witness-generator" -version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" -dependencies = [ - "tempfile", + "thiserror 2.0.18", ] [[package]] name = "logos-blockchain-zksign" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-signature-sys", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-poseidon2", - "logos-blockchain-witness-generator", + "logos-blockchain-proofs-error", "num-bigint", "serde", "serde_json", @@ -4023,17 +4258,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "maybe-async" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "memchr" version = "2.8.0" @@ -4047,7 +4271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", - "keccak", + "keccak 0.1.6", "rand_core 0.6.4", "zeroize", ] @@ -4079,6 +4303,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.0" @@ -4090,6 +4324,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha3", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + [[package]] name = "moka" version = "0.12.15" @@ -4286,10 +4545,10 @@ dependencies = [ "ark-ec 0.4.2", "ark-ff 0.4.2", "ark-serialize 0.4.2", - "digest", + "digest 0.10.7", "generic-array 0.14.7", "hex", - "keccak", + "keccak 0.1.6", "log", "rand 0.8.5", "zeroize", @@ -4332,51 +4591,13 @@ dependencies = [ "memchr", ] -[[package]] -name = "nssa" -version = "0.1.0" -dependencies = [ - "anyhow", - "borsh", - "clock_core", - "faucet_core", - "hex", - "k256", - "log", - "nssa_core", - "rand 0.8.5", - "risc0-binfmt", - "risc0-build", - "risc0-zkvm", - "serde", - "serde_with", - "sha2", - "thiserror 2.0.18", -] - -[[package]] -name = "nssa_core" -version = "0.1.0" -dependencies = [ - "base58", - "borsh", - "bytemuck", - "bytesize", - "chacha20", - "k256", - "risc0-zkvm", - "serde", - "serde_with", - "thiserror 2.0.18", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4709,9 +4930,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -4720,8 +4941,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", ] [[package]] @@ -4811,6 +5042,30 @@ dependencies = [ "toml_edit 0.25.11+spec-1.1.0", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -4842,18 +5097,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "version_check", -] - [[package]] name = "prometheus-client" version = "0.22.3" @@ -5115,6 +5358,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -5214,7 +5463,6 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", - "futures-channel", "futures-core", "futures-util", "h2", @@ -5427,7 +5675,7 @@ dependencies = [ "borsh", "bytemuck", "cfg-if", - "digest", + "digest 0.10.7", "hex", "hex-literal", "metal", @@ -5449,7 +5697,6 @@ checksum = "22b7eafb5d85be59cbd9da83f662cf47d834f1b836e14f675d1530b12c666867" dependencies = [ "anyhow", "bincode", - "bonsai-sdk", "borsh", "bytemuck", "bytes", @@ -5538,16 +5785,16 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "signature", - "spki", + "spki 0.7.3", "subtle", "zeroize", ] @@ -5626,7 +5873,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5635,6 +5882,7 @@ version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -5755,9 +6003,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.10", "generic-array 0.14.7", - "pkcs8", + "pkcs8 0.10.2", "serdect", "subtle", "zeroize", @@ -5897,6 +6145,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serdect" version = "0.2.0" @@ -5915,7 +6176,17 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", ] [[package]] @@ -5949,10 +6220,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.12" @@ -5998,7 +6275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6014,7 +6291,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", ] [[package]] @@ -6072,6 +6359,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -6152,6 +6445,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -6162,7 +6466,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6171,8 +6475,8 @@ version = "0.1.0" dependencies = [ "common", "key_protocol", - "nssa", - "nssa_core", + "lee", + "lee_core", "serde", ] @@ -6524,11 +6828,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", + "symlink", "thiserror 2.0.18", "time", "tracing-subscriber 0.3.23", @@ -6741,6 +7046,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "unsigned-varint" version = "0.7.2" @@ -6759,6 +7070,35 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http 1.4.0", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -6772,12 +7112,42 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utoipa" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "uuid" version = "1.23.1" @@ -6789,6 +7159,36 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling 0.20.11", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "valuable" version = "0.1.1" @@ -7420,6 +7820,16 @@ dependencies = [ "time", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xdg" version = "3.0.0" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index d2d8e873..de3cb487 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -44,11 +44,11 @@ libfuzzer-sys = { version = "0.4", optional = true } afl = { version = "0.15", optional = true } arbitrary = { version = "1", features = ["derive"] } borsh = "1" -nssa = { path = "../../logos-execution-zone/nssa" } -nssa_core = { path = "../../logos-execution-zone/nssa/core" } -common = { path = "../../logos-execution-zone/common" } +nssa = { path = "../../logos-execution-zone/lee/state_machine", package = "lee" } +nssa_core = { path = "../../logos-execution-zone/lee/state_machine/core", package = "lee_core" } +common = { path = "../../logos-execution-zone/lez/common" } fuzz_props = { path = "../fuzz_props" } -testnet_initial_state = { path = "../../logos-execution-zone/testnet_initial_state" } +testnet_initial_state = { path = "../../logos-execution-zone/lez/testnet_initial_state" } [profile.release] debug = true diff --git a/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs b/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs index 7cd5fe8a..3564cae0 100644 --- a/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs +++ b/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs @@ -33,7 +33,7 @@ use std::collections::HashSet; use arbitrary::{Arbitrary, Unstructured}; -use fuzz_props::arbitrary_types::ArbNSSATransaction; +use fuzz_props::arbitrary_types::ArbLeeTransaction; use fuzz_props::generators::{arbitrary_fuzz_state, signer_account_ids}; use fuzz_props::invariants::{NonceSnapshot, assert_nonce_increment_correctness}; use nssa::V03State; @@ -52,7 +52,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { .collect(); // Generate and stateless-check a transaction. - let tx_raw = match ArbNSSATransaction::arbitrary(&mut u) { + let tx_raw = match ArbLeeTransaction::arbitrary(&mut u) { Ok(w) => w.0, Err(_) => return, }; diff --git a/fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs b/fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs index 1cf693a2..9f20ddac 100644 --- a/fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs +++ b/fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs @@ -38,7 +38,7 @@ use std::collections::HashSet; use arbitrary::{Arbitrary, Unstructured}; -use common::transaction::{NSSATransaction, clock_invocation}; +use common::transaction::{LeeTransaction, clock_invocation}; use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction}; use nssa::V03State; @@ -81,7 +81,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { // Accepted transaction list — populated here, consumed by the replayer phase // so that both pipelines process exactly the same set of transactions. - let mut accepted_txs: Vec = Vec::new(); + let mut accepted_txs: Vec = Vec::new(); let n_txs: u8 = u8::arbitrary(&mut u).unwrap_or(0) % 8; diff --git a/fuzz/fuzz_targets/fuzz_state_diff_computation.rs b/fuzz/fuzz_targets/fuzz_state_diff_computation.rs index 0c912fe2..e2520465 100644 --- a/fuzz/fuzz_targets/fuzz_state_diff_computation.rs +++ b/fuzz/fuzz_targets/fuzz_state_diff_computation.rs @@ -19,7 +19,7 @@ //! specific account shapes such as zero balance or `u128::MAX` — are reachable. use arbitrary::{Arbitrary, Unstructured}; -use common::transaction::NSSATransaction; +use common::transaction::LeeTransaction; use fuzz_props::arbitrary_types::ArbPublicTransaction; use fuzz_props::generators::arbitrary_fuzz_state; use nssa::{V03State, ValidatedStateDiff}; @@ -47,7 +47,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { // Collect the set of accounts the transaction declares it will touch. // `affected_public_account_ids()` returns owned data so `pub_tx` remains // available for both `from_public_transaction` (borrow) and the later move - // into `NSSATransaction::Public`. + // into `LeeTransaction::Public`. let affected = pub_tx.affected_public_account_ids(); match ValidatedStateDiff::from_public_transaction(&pub_tx, &state, 1, 0) { @@ -77,7 +77,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { // we do not panic on a structurally malformed transaction. let mut exec_state = state.clone(); // `pub_tx` is moved here; it is no longer borrowed after this point. - let tx_for_exec = NSSATransaction::Public(pub_tx); + let tx_for_exec = LeeTransaction::Public(pub_tx); if let Ok(checked_tx) = tx_for_exec.transaction_stateless_check() { if checked_tx.execute_check_on_state(&mut exec_state, 1, 0).is_ok() { for acc_id in &affected { diff --git a/fuzz/fuzz_targets/fuzz_stateless_verification.rs b/fuzz/fuzz_targets/fuzz_stateless_verification.rs index 767edf1c..830317c4 100644 --- a/fuzz/fuzz_targets/fuzz_stateless_verification.rs +++ b/fuzz/fuzz_targets/fuzz_stateless_verification.rs @@ -1,7 +1,7 @@ #![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] use arbitrary::Unstructured; -use common::transaction::NSSATransaction; +use common::transaction::LeeTransaction; use fuzz_props::generators::arbitrary_transaction; fuzz_props::fuzz_entry!(|data: &[u8]| { @@ -22,7 +22,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { } // Path B: raw decode first, then check — must never panic - if let Ok(tx) = borsh::from_slice::(data) { + if let Ok(tx) = borsh::from_slice::(data) { let _ = tx.transaction_stateless_check(); } }); diff --git a/fuzz/fuzz_targets/fuzz_transaction_decoding.rs b/fuzz/fuzz_targets/fuzz_transaction_decoding.rs index 07a43023..bfa54239 100644 --- a/fuzz/fuzz_targets/fuzz_transaction_decoding.rs +++ b/fuzz/fuzz_targets/fuzz_transaction_decoding.rs @@ -2,19 +2,19 @@ use common::{ block::{Block, HashableBlockData}, - transaction::NSSATransaction, + transaction::LeeTransaction, }; fuzz_props::fuzz_entry!(|data: &[u8]| { - // Attempt 1: decode as NSSATransaction and verify roundtrip - if let Ok(tx) = borsh::from_slice::(data) { + // Attempt 1: decode as LeeTransaction and verify roundtrip + if let Ok(tx) = borsh::from_slice::(data) { let re_encoded = borsh::to_vec(&tx).expect("re-encode of valid tx must succeed"); - let tx2 = borsh::from_slice::(&re_encoded) + let tx2 = borsh::from_slice::(&re_encoded) .expect("second decode of re-encoded tx must succeed"); assert_eq!( re_encoded, borsh::to_vec(&tx2).unwrap(), - "NSSATransaction roundtrip encoding divergence" + "LeeTransaction roundtrip encoding divergence" ); } diff --git a/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs b/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs index 75d8e689..522154db 100644 --- a/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs +++ b/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs @@ -25,7 +25,7 @@ //! reachable by the fuzzer. use arbitrary::{Arbitrary, Unstructured}; -use fuzz_props::arbitrary_types::ArbNSSATransaction; +use fuzz_props::arbitrary_types::ArbLeeTransaction; use fuzz_props::generators::{arbitrary_fuzz_state, signer_account_ids}; use fuzz_props::invariants::{NonceSnapshot, assert_nonce_increment_correctness}; use nssa::V03State; @@ -47,7 +47,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { .collect(); // Generate the transaction from the remaining fuzz bytes. - let tx = match ArbNSSATransaction::arbitrary(&mut u) { + let tx = match ArbLeeTransaction::arbitrary(&mut u) { Ok(w) => w.0, Err(_) => return, }; diff --git a/fuzz_props/src/arbitrary_types.rs b/fuzz_props/src/arbitrary_types.rs index d920d6cc..20ea8430 100644 --- a/fuzz_props/src/arbitrary_types.rs +++ b/fuzz_props/src/arbitrary_types.rs @@ -2,7 +2,7 @@ //! //! **No changes to `../logos-execution-zone` are required.** //! -//! The Rust orphan rule forbids `impl Arbitrary for NSSATransaction` when both +//! The Rust orphan rule forbids `impl Arbitrary for LeeTransaction` when both //! the trait and the type come from external crates. Using newtypes (`ArbXxx`) //! sidesteps the restriction entirely. //! @@ -10,10 +10,10 @@ //! //! ```rust,ignore //! #![no_main] -//! use fuzz_props::arbitrary_types::ArbNSSATransaction; +//! use fuzz_props::arbitrary_types::ArbLeeTransaction; //! use libfuzzer_sys::fuzz_target; //! -//! fuzz_target!(|wrapped: ArbNSSATransaction| { +//! fuzz_target!(|wrapped: ArbLeeTransaction| { //! let tx = wrapped.0; //! let Ok(valid_tx) = tx.transaction_stateless_check() else { return; }; //! // … @@ -21,7 +21,7 @@ //! ``` use arbitrary::{Arbitrary, Result as ArbResult, Unstructured}; -use common::{HashType, block::HashableBlockData, transaction::NSSATransaction}; +use common::{HashType, block::HashableBlockData, transaction::LeeTransaction}; use nssa::{ AccountId, PrivateKey, PublicKey, Signature, program_deployment_transaction::ProgramDeploymentTransaction, @@ -210,24 +210,24 @@ impl<'a> Arbitrary<'a> for ArbProgramDeploymentTransaction { } } -// ── NSSATransaction ─────────────────────────────────────────────────────────── +// ── LeeTransaction ─────────────────────────────────────────────────────────── // `PrivacyPreservingTransaction` is intentionally excluded: it embeds a risc0 // ZK receipt that cannot be generated inside a hot fuzzing loop. This matches // the known limitation documented in `docs/fuzzing.md`. -/// Newtype wrapper providing [`Arbitrary`] for [`NSSATransaction`]. +/// Newtype wrapper providing [`Arbitrary`] for [`LeeTransaction`]. /// /// Generates `Public` and `ProgramDeployment` variants only. #[derive(Debug)] -pub struct ArbNSSATransaction(pub NSSATransaction); +pub struct ArbLeeTransaction(pub LeeTransaction); -impl<'a> Arbitrary<'a> for ArbNSSATransaction { +impl<'a> Arbitrary<'a> for ArbLeeTransaction { fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult { match u8::arbitrary(u)? % 2 { - 0 => Ok(Self(NSSATransaction::Public( + 0 => Ok(Self(LeeTransaction::Public( ArbPublicTransaction::arbitrary(u)?.0, ))), - _ => Ok(Self(NSSATransaction::ProgramDeployment( + _ => Ok(Self(LeeTransaction::ProgramDeployment( ArbProgramDeploymentTransaction::arbitrary(u)?.0, ))), } @@ -246,7 +246,7 @@ impl<'a> Arbitrary<'a> for ArbHashableBlockData { fn arbitrary(u: &mut Unstructured<'a>) -> ArbResult { // 0–7 transactions per block let n = (u8::arbitrary(u)? as usize) % 8; - let transactions = std::iter::repeat_with(|| ArbNSSATransaction::arbitrary(u).map(|t| t.0)) + let transactions = std::iter::repeat_with(|| ArbLeeTransaction::arbitrary(u).map(|t| t.0)) .take(n) .collect::>>()?; Ok(Self(HashableBlockData { diff --git a/fuzz_props/src/generators.rs b/fuzz_props/src/generators.rs index 8d759668..d11df5a9 100644 --- a/fuzz_props/src/generators.rs +++ b/fuzz_props/src/generators.rs @@ -1,8 +1,8 @@ use arbitrary::{Arbitrary, Unstructured}; -use common::{block::HashableBlockData, transaction::NSSATransaction}; +use common::{block::HashableBlockData, transaction::LeeTransaction}; use nssa::{AccountId, PrivateKey}; -use crate::arbitrary_types::{ArbAccountId, ArbNSSATransaction, ArbPrivateKey}; +use crate::arbitrary_types::{ArbAccountId, ArbLeeTransaction, ArbPrivateKey}; use proptest::prelude::*; use testnet_initial_state::initial_pub_accounts_private_keys; @@ -12,22 +12,22 @@ use testnet_initial_state::initial_pub_accounts_private_keys; /// witness set. Used by fuzz targets that need to verify nonce /// increments after `execute_check_on_state`. #[must_use] -pub fn signer_account_ids(tx: &common::transaction::NSSATransaction) -> Vec { - use common::transaction::NSSATransaction; +pub fn signer_account_ids(tx: &common::transaction::LeeTransaction) -> Vec { + use common::transaction::LeeTransaction; match tx { - NSSATransaction::Public(pt) => pt + LeeTransaction::Public(pt) => pt .witness_set() .signatures_and_public_keys() .iter() .map(|(_, pk)| nssa::AccountId::from(pk)) .collect(), - NSSATransaction::PrivacyPreserving(pt) => pt + LeeTransaction::PrivacyPreserving(pt) => pt .witness_set() .signatures_and_public_keys() .iter() .map(|(_, pk)| nssa::AccountId::from(pk)) .collect(), - NSSATransaction::ProgramDeployment(_) => vec![], + LeeTransaction::ProgramDeployment(_) => vec![], } } @@ -74,7 +74,7 @@ pub fn arbitrary_fuzz_state(u: &mut Unstructured<'_>) -> arbitrary::Result) -> arbitrary::Result, accounts: &[FuzzAccount], -) -> arbitrary::Result { +) -> arbitrary::Result { if accounts.is_empty() { return Err(arbitrary::Error::IncorrectFormat); } @@ -112,9 +112,9 @@ pub fn arb_fuzz_native_transfer( // ── Arbitrary (for libFuzzer targets) ──────────────────────────────────────── -/// Generate a structurally plausible `NSSATransaction` from unstructured bytes. -pub fn arbitrary_transaction(u: &mut Unstructured<'_>) -> arbitrary::Result { - ArbNSSATransaction::arbitrary(u).map(|w| w.0) +/// Generate a structurally plausible `LeeTransaction` from unstructured bytes. +pub fn arbitrary_transaction(u: &mut Unstructured<'_>) -> arbitrary::Result { + ArbLeeTransaction::arbitrary(u).map(|w| w.0) } // ── proptest strategies ─────────────────────────────────────────────────────── @@ -128,7 +128,7 @@ prop_compose! { to_idx in 0..accounts.len(), nonce in 0_u128..1_000_u128, amount in 0_u128..10_000_u128, - ) -> NSSATransaction { + ) -> LeeTransaction { let (from_id, from_key) = &accounts[from_idx]; let (to_id, _) = &accounts[to_idx]; common::test_utils::create_transaction_native_token_transfer( @@ -146,11 +146,11 @@ pub fn test_accounts() -> Vec<(AccountId, PrivateKey)> { .collect() } -/// Strategy: raw bytes that are valid borsh encodings of `NSSATransaction`. +/// Strategy: raw bytes that are valid borsh encodings of `LeeTransaction`. pub fn arb_borsh_transaction_bytes() -> impl Strategy> { any::>().prop_map(|bytes| { // Either pass through raw bytes OR encode a known dummy transaction - if borsh::from_slice::(&bytes).is_ok() { + if borsh::from_slice::(&bytes).is_ok() { bytes } else { borsh::to_vec(&common::test_utils::produce_dummy_empty_transaction()).unwrap() @@ -183,7 +183,7 @@ prop_compose! { phantom_id_bytes in proptest::array::uniform32(0_u8..), amount in (u128::MAX / 2)..u128::MAX, // overflow-inducing amount nonce in 0_u128..10_u128, - ) -> NSSATransaction { + ) -> LeeTransaction { let phantom_id = nssa::AccountId::new(phantom_id_bytes); // Attempt to sign with a key that has no matching on-chain account let signing_key = nssa::PrivateKey::try_new(phantom_id_bytes) @@ -204,11 +204,11 @@ prop_compose! { /// attack candidates) and some are re-ordered permutations of a valid sequence. /// Used in proptest-level tests and as a seed generator for the state-transition /// fuzz target. -pub fn arb_duplicate_tx_sequence() -> impl Strategy> { +pub fn arb_duplicate_tx_sequence() -> impl Strategy> { let accounts = test_accounts(); proptest::collection::vec(arb_native_transfer_tx(accounts), 1..5_usize).prop_flat_map(|txs| { // Build a sequence that: original | duplicates | reversed - let duped: Vec = txs + let duped: Vec = txs .iter() .cloned() .chain(txs.iter().cloned()) // append exact duplicates @@ -225,7 +225,7 @@ pub fn arb_duplicate_tx_sequence() -> impl Strategy /// - self-transfers (sender == recipient), /// - max-nonce wrapping, /// - alternating valid / invalid transactions to test partial-batch isolation. -pub fn arb_pathological_sequence() -> impl Strategy> { +pub fn arb_pathological_sequence() -> impl Strategy> { let accounts = test_accounts(); let n = accounts.len(); proptest::collection::vec((0..n, 0..n, 0_u128..5_u128, any::()), 1..8_usize).prop_map( diff --git a/fuzz_props/src/invariants.rs b/fuzz_props/src/invariants.rs index b874879b..d520e30d 100644 --- a/fuzz_props/src/invariants.rs +++ b/fuzz_props/src/invariants.rs @@ -1,4 +1,4 @@ -use common::transaction::NSSATransaction; +use common::transaction::LeeTransaction; use nssa::V03State; use nssa_core::account::Nonce; @@ -185,7 +185,7 @@ impl ProtocolInvariant for FailedTxNonceStability { /// # Enforcement /// /// This invariant **cannot** be enforced through [`InvariantCtx`] because the replay -/// check requires re-applying the `NSSATransaction` that `execute_check_on_state` +/// check requires re-applying the `LeeTransaction` that `execute_check_on_state` /// consumes and returns on `Ok`. It is therefore **not registered** in /// [`assert_invariants`]; calling `assert_invariants` alone does **not** cover /// `ReplayRejection`. @@ -235,7 +235,7 @@ pub struct NonceIncrementCorrectness; /// /// # Why a standalone function? /// -/// `execute_check_on_state` consumes the `NSSATransaction` and returns it on `Ok`, +/// `execute_check_on_state` consumes the `LeeTransaction` and returns it on `Ok`, /// so the transaction is not available as a shared reference inside [`InvariantCtx`]. /// This function accepts ownership of the returned transaction and performs the /// replay in-place. @@ -249,7 +249,7 @@ pub struct NonceIncrementCorrectness; /// } /// ``` pub fn assert_replay_rejection( - applied_tx: NSSATransaction, + applied_tx: LeeTransaction, state: &mut V03State, next_block_id: u64, next_timestamp: u64, @@ -270,7 +270,7 @@ pub fn assert_replay_rejection( /// passing the signer IDs derived from the transaction's witness set, the [`NonceSnapshot`] /// captured **before** execution, and the post-execution state. /// -/// For a `NSSATransaction::Public(tx)`, derive signer IDs as: +/// For a `LeeTransaction::Public(tx)`, derive signer IDs as: /// /// ```rust,ignore /// let signer_ids: Vec = tx @@ -281,7 +281,7 @@ pub fn assert_replay_rejection( /// .collect(); /// ``` /// -/// For `NSSATransaction::ProgramDeployment`, there are no signers; pass an empty slice. +/// For `LeeTransaction::ProgramDeployment`, there are no signers; pass an empty slice. /// /// # Why a standalone function? /// @@ -377,7 +377,7 @@ pub fn assert_tx_execution_invariants( state_after: &mut V03State, balances_before: BalanceSnapshot, nonces_before: NonceSnapshot, - execution_result: Result, + execution_result: Result, replay_context: (u64, u64), ) { let execution_succeeded = execution_result.is_ok(); @@ -400,19 +400,19 @@ pub fn assert_tx_execution_invariants( if let Ok(applied_tx) = execution_result { // Derive signer IDs from the witness set. ProgramDeployment has no signers. let signer_ids: Vec = match &applied_tx { - NSSATransaction::Public(pt) => pt + LeeTransaction::Public(pt) => pt .witness_set() .signatures_and_public_keys() .iter() .map(|(_, pk)| nssa::AccountId::from(pk)) .collect(), - NSSATransaction::PrivacyPreserving(pt) => pt + LeeTransaction::PrivacyPreserving(pt) => pt .witness_set() .signatures_and_public_keys() .iter() .map(|(_, pk)| nssa::AccountId::from(pk)) .collect(), - NSSATransaction::ProgramDeployment(_) => vec![], + LeeTransaction::ProgramDeployment(_) => vec![], }; assert_nonce_increment_correctness(&signer_ids, &nonces_for_nonce_check, state_after); let (next_block_id, next_timestamp) = replay_context; From a8d0355b9fcb329371e011f80f295271a5060615 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 5 Jun 2026 18:19:32 +0800 Subject: [PATCH 09/31] fix: address mutants found in harness --- fuzz_props/src/tests.rs | 2 + fuzz_props/src/tests/arbitrary_types_test.rs | 158 ++++++++++++++++ fuzz_props/src/tests/generators_test.rs | 135 ++++++++++++++ fuzz_props/src/tests/invariants.rs | 184 ++++++++++++++++++- 4 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 fuzz_props/src/tests/arbitrary_types_test.rs create mode 100644 fuzz_props/src/tests/generators_test.rs diff --git a/fuzz_props/src/tests.rs b/fuzz_props/src/tests.rs index 759db831..af7a7e6c 100644 --- a/fuzz_props/src/tests.rs +++ b/fuzz_props/src/tests.rs @@ -1,3 +1,5 @@ +mod arbitrary_types_test; +mod generators_test; mod invariants; mod replay_proptest; mod seed_gen; diff --git a/fuzz_props/src/tests/arbitrary_types_test.rs b/fuzz_props/src/tests/arbitrary_types_test.rs new file mode 100644 index 00000000..fc8c57a7 --- /dev/null +++ b/fuzz_props/src/tests/arbitrary_types_test.rs @@ -0,0 +1,158 @@ +//! Tests that detect mutations in `arbitrary_types.rs`. +//! +//! # Design rationale +//! +//! `arbitrary::Unstructured::fill_buffer` reads bytes from the **front** of the buffer +//! and pads with zeros when the buffer is exhausted — it never returns an error. As a +//! result, the total number of items generated by `take(n)` always equals `n` regardless +//! of buffer size. This makes count-based tests the most reliable mutation detectors. +//! +//! For types that expose their length through public APIs we check the count directly. +//! For `ArbPubTxMessage`, whose inner [`nssa::public_transaction::Message`] is opaque, +//! we use the borsh-serialised size of a wrapping [`LeeTransaction::Public`] as a proxy. + +use crate::arbitrary_types::{ + ArbHashableBlockData, ArbLeeTransaction, ArbPubTxMessage, ArbPublicTransaction, ArbWitnessSet, +}; +use arbitrary::{Arbitrary, Unstructured}; +use common::transaction::LeeTransaction; + +#[test] +fn arb_lee_transaction_zero_byte_selects_public() { + // fill_buffer reads from the front, so the first byte consumed = 0. + let buf = vec![0_u8; 4096]; + let mut u = Unstructured::new(&buf); + let arb = ArbLeeTransaction::arbitrary(&mut u).expect("should succeed"); + assert!( + matches!(arb.0, LeeTransaction::Public(_)), + "expected Public variant: with first byte=0 and `% 2`, arm 0 (Public) is selected" + ); +} + +#[test] +fn arb_lee_transaction_byte4_selects_public() { + // Place 4 as the first byte (variant selector); rest are zeros. + let mut buf = vec![0_u8; 4096]; + buf[0] = 4; + let mut u = Unstructured::new(&buf); + let arb = ArbLeeTransaction::arbitrary(&mut u).expect("should succeed"); + assert!( + matches!(arb.0, LeeTransaction::Public(_)), + "expected Public variant: `4 % 2 = 0` \u{2192} arm 0; \ + mutant `4 / 2 = 2` or `4 + 2 = 6` maps to `_` \u{2192} ProgramDeployment" + ); +} + +/// Generates from all-1 bytes: `1 % 2 = 1` -> `_` -> `ProgramDeployment`. +#[test] +fn arb_lee_transaction_one_byte_selects_program_deployment() { + let buf = vec![1_u8; 4096]; + let mut u = Unstructured::new(&buf); + let arb = ArbLeeTransaction::arbitrary(&mut u).expect("should succeed"); + assert!( + matches!(arb.0, LeeTransaction::ProgramDeployment(_)), + "expected ProgramDeployment variant with first byte=1" + ); +} + +/// Generates `ArbHashableBlockData` from all-255 bytes and asserts `transactions.len() <= 7`. +#[test] +fn arb_hashable_block_data_tx_count_bounded() { + let buf = vec![255_u8; 50_000]; + let mut u = Unstructured::new(&buf); + let arb = ArbHashableBlockData::arbitrary(&mut u) + .expect("ArbHashableBlockData::arbitrary should succeed with a large all-255 buffer"); + assert!( + arb.0.transactions.len() <= 7, + "expected at most 7 transactions (% 8), got {} \ + (mutation: % replaced by / or + on line 248 of arbitrary_types.rs)", + arb.0.transactions.len() + ); +} + +/// Generates `ArbWitnessSet` from all-255 bytes and asserts pair count <= 3. +#[test] +fn arb_witness_set_pair_count_bounded() { + let buf = vec![255_u8; 50_000]; + let mut u = Unstructured::new(&buf); + let arb = ArbWitnessSet::arbitrary(&mut u) + .expect("ArbWitnessSet::arbitrary should succeed with a large all-255 buffer"); + let pair_count = arb.0.signatures_and_public_keys().len(); + assert!( + pair_count <= 3, + "expected at most 3 witness pairs (% 4), got {pair_count} \ + (mutation: % replaced by / or + on line 173 of arbitrary_types.rs)" + ); +} + +/// Checks the borsh-encoded size of a `LeeTransaction::Public` wrapping an +/// `ArbPubTxMessage` generated from a buffer where the len-selector byte = 255. +#[test] +fn arb_pub_tx_message_account_count_bounded_via_borsh() { + // Bytes 0-31: zeros for program_id ([u32; 8] via fill_buffer reads 32 bytes). + // Byte 32: 255 — this is the len-selector byte. 255 % 8 = 7 (correct) vs 255 / 8 = 31 (mutant). + // Bytes 33+: zeros so Vec (instruction_data) produces 0 elements (last byte = 0). + let mut buf = vec![0_u8; 2000]; + buf[32] = 255; + + let mut u = Unstructured::new(&buf); + let msg = + ArbPubTxMessage::arbitrary(&mut u).expect("ArbPubTxMessage::arbitrary should succeed"); + + // Wrap in a real PublicTransaction to enable borsh serialisation. + let mut u_witness = Unstructured::new(&[0_u8; 10]); + let witness = ArbWitnessSet::arbitrary(&mut u_witness) + .expect("ArbWitnessSet::arbitrary should succeed with zero bytes (n=0)"); + + let tx = LeeTransaction::Public(nssa::public_transaction::PublicTransaction::new( + msg.0, witness.0, + )); + let borsh_bytes = borsh::to_vec(&tx).expect("borsh serialization should succeed"); + + // With 7 accounts the message borsh-encodes to ~380 bytes; the whole transaction to ~400 bytes. + // With 31 accounts the message encodes to ~1540 bytes. + // Using 800 as a conservative threshold clearly separates the two cases. + assert!( + borsh_bytes.len() < 800, + "borsh-encoded size {} bytes exceeds threshold: too many accounts in message \ + (% 8 may have been replaced with / 8 or + 8 on line 144 of arbitrary_types.rs)", + borsh_bytes.len() + ); +} + +/// Additional check: with an all-zero buffer the `ArbPubTxMessage` generates a +/// message with 0 accounts (`0 % 8 = 0`). This verifies the zero case. +#[test] +fn arb_pub_tx_message_zero_accounts_with_zero_selector() { + // All zeros: program_id = all 0, len selector byte = 0. + // 0 % 8 = 0 (correct), 0 + 8 = 8 (+ mutant). + let buf = vec![0_u8; 500]; + let mut u = Unstructured::new(&buf); + let msg = + ArbPubTxMessage::arbitrary(&mut u).expect("ArbPubTxMessage::arbitrary should succeed"); + + let mut u_witness = Unstructured::new(&[0_u8; 10]); + let witness = ArbWitnessSet::arbitrary(&mut u_witness).expect("witness should succeed"); + + let tx = LeeTransaction::Public(nssa::public_transaction::PublicTransaction::new( + msg.0, witness.0, + )); + let borsh_bytes = borsh::to_vec(&tx).expect("borsh serialization should succeed"); + + // With 0 accounts borsh size is minimal (~50 bytes for empty message + envelope). + // With 8 accounts (+ mutant) borsh size > 400 bytes. + assert!( + borsh_bytes.len() < 300, + "borsh-encoded size {} bytes too large for zero-account message \ + (% 8 may have been replaced with + 8 on line 144 of arbitrary_types.rs)", + borsh_bytes.len() + ); +} + +/// Verifies that `ArbPublicTransaction::arbitrary` completes without error. +#[test] +fn arb_public_transaction_smoke() { + let buf = vec![0_u8; 4096]; + let mut u = Unstructured::new(&buf); + let _ = ArbPublicTransaction::arbitrary(&mut u).expect("should succeed"); +} diff --git a/fuzz_props/src/tests/generators_test.rs b/fuzz_props/src/tests/generators_test.rs new file mode 100644 index 00000000..44c7b139 --- /dev/null +++ b/fuzz_props/src/tests/generators_test.rs @@ -0,0 +1,135 @@ +//! Tests that detect mutations in `generators.rs`. + +use arbitrary::Unstructured; +use nssa::{AccountId, PrivateKey}; + +use crate::generators::{ + FuzzAccount, arb_fuzz_native_transfer, arbitrary_fuzz_state, signer_account_ids, test_accounts, +}; + +/// Verifies that `signer_account_ids` returns a **non-empty** list for a properly signed +/// public transaction. +#[test] +fn signer_ids_nonempty_for_signed_public_tx() { + let accounts = test_accounts(); + let (from_id, from_key) = &accounts[0]; + let (to_id, _) = &accounts[1]; + + let tx = common::test_utils::create_transaction_native_token_transfer( + *from_id, 0, // nonce 0 — genesis nonce for the account + *to_id, 100, from_key, + ); + + let ids = signer_account_ids(&tx); + assert!( + !ids.is_empty(), + "signer_account_ids must return at least one ID for a signed public transaction \ + (mutation: function body replaced with vec![])" + ); +} + +/// Verifies that the returned signer ID matches the account that actually signed the +/// transaction — not a default/zeroed account ID. +#[test] +fn signer_ids_contains_the_signing_account() { + let accounts = test_accounts(); + let (from_id, from_key) = &accounts[0]; + let (to_id, _) = &accounts[1]; + + let tx = common::test_utils::create_transaction_native_token_transfer( + *from_id, 0, *to_id, 100, from_key, + ); + + let ids = signer_account_ids(&tx); + assert!( + ids.contains(from_id), + "signer_account_ids must contain the account ID of the private key that signed \ + the transaction; got {ids:?} but expected it to contain {from_id:?}" + ); +} + +#[test] +fn fuzz_state_never_empty() { + let buf = vec![0_u8; 1000]; + let mut u = Unstructured::new(&buf); + let accounts = arbitrary_fuzz_state(&mut u).expect("should succeed"); + assert!( + !accounts.is_empty(), + "arbitrary_fuzz_state must return at least 1 account (n = 1..=8); \ + returned 0 \u{2014} mutation: `+ 1` replaced by `* 1` or `Ok(vec![])`" + ); +} + +#[test] +fn fuzz_state_count_uses_modulo_not_div_or_add() { + // fill_buffer reads from the front; the first byte is the n-selector. + let mut buf = vec![0_u8; 1000]; + buf[0] = 8; // selector byte: 8 % 8 = 0, +1 -> n=1 | 8 / 8 = 1, +1 -> n=2 | 8 + 8 = 16, +1 -> n=17 + let mut u = Unstructured::new(&buf); + let accounts = arbitrary_fuzz_state(&mut u).expect("should succeed"); + assert_eq!( + accounts.len(), + 1, + "with selector byte=8: (8 % 8) + 1 = 1 account; \ + mutation `% \u{2192} /` gives (8/8)+1=2; mutation `% \u{2192} +` gives (8+8)+1=17" + ); +} + +/// Verifies that each account's balance is <= `u128::MAX / 8`. +#[test] +fn fuzz_state_balances_bounded_by_max_div_8() { + let buf = vec![255_u8; 10_000]; + let mut u = Unstructured::new(&buf); + // With correct division, this must NOT overflow (no panic). + let accounts = arbitrary_fuzz_state(&mut u) + .expect("should succeed \u{2014} no overflow with correct / 8 implementation"); + + let max_balance = u128::MAX / 8; + for acc in &accounts { + assert!( + acc.balance <= max_balance, + "account balance {} exceeds u128::MAX/8={} \u{2014} \ + mutation: `/ 8` replaced by `* 8` (overflow) or `% 8`", + acc.balance, + max_balance + ); + } + + // Ensures the `% 8` mutation is caught: with u128::MAX bytes, correct `/` gives a + // large balance (u128::MAX/8 ~= 3.4e37), while `%` gives only 0-7. + let has_large_balance = accounts.iter().any(|a| a.balance > 7); + assert!( + has_large_balance, + "expected at least one account with balance > 7 \u{2014} \ + mutation: `/ 8` replaced by `% 8` (balance capped at 7)" + ); +} + +#[test] +fn native_transfer_index_uses_modulo_not_div_add() { + let accounts = vec![ + FuzzAccount { + account_id: AccountId::new([1_u8; 32]), + balance: 1_000_000, + private_key: PrivateKey::try_new([1_u8; 32]).expect("scalar 1 is a valid private key"), + }, + FuzzAccount { + account_id: AccountId::new([2_u8; 32]), + balance: 1_000_000, + private_key: PrivateKey::try_new([2_u8; 32]).expect("scalar 2 is a valid private key"), + }, + ]; + + // All-0xFF bytes: the from_idx byte = 255, to_idx byte = 255. + // 255 % 2 = 1 (in-bounds), 255 / 2 = 127 (out-of-bounds), 255 + 2 = 257 (out-of-bounds). + let buf = vec![0xFF_u8; 500]; + let mut u = Unstructured::new(&buf); + + // With the mutated `/ 2` or `+ 2`, `accounts[127]` or `accounts[257]` panics. + let result = arb_fuzz_native_transfer(&mut u, &accounts); + assert!( + result.is_ok(), + "arb_fuzz_native_transfer should succeed with valid modulo-bounded indices; \ + mutation: `% accounts.len()` replaced by `/ accounts.len()` or `+ accounts.len()`" + ); +} diff --git a/fuzz_props/src/tests/invariants.rs b/fuzz_props/src/tests/invariants.rs index 178b9342..1d799abd 100644 --- a/fuzz_props/src/tests/invariants.rs +++ b/fuzz_props/src/tests/invariants.rs @@ -1,7 +1,10 @@ +use crate::generators::test_accounts; use crate::invariants::{ - BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, - assert_nonce_increment_correctness, + BalanceConservation, BalanceSnapshot, FailedTxNonceStability, InvariantCtx, NonceSnapshot, + ProtocolInvariant, StateIsolationOnFailure, assert_invariants, + assert_nonce_increment_correctness, assert_replay_rejection, assert_tx_execution_invariants, }; +use common::transaction::LeeTransaction; use nssa::V03State; use nssa_core::account::Nonce; @@ -117,3 +120,180 @@ fn failed_tx_nonce_stability_catches_nonce_mutation() { "expected panic for nonce mutation on failure" ); } + +/// Verifies that `BalanceSnapshot::total` returns the correct arithmetical sum. +#[test] +fn balance_snapshot_total_is_correct_sum() { + let mut map = std::collections::HashMap::new(); + map.insert(nssa::AccountId::new([1_u8; 32]), 100_u128); + map.insert(nssa::AccountId::new([2_u8; 32]), 200_u128); + map.insert(nssa::AccountId::new([3_u8; 32]), 700_u128); + let snap = BalanceSnapshot(map); + assert_eq!( + snap.total(), + 1000, + "BalanceSnapshot::total must sum all balances" + ); +} + +/// Ensures `total()` is non-zero when accounts have positive balances. +/// +/// Together with `balance_snapshot_total_is_correct_sum`, this forms a pair that +/// catches the `replace total with 0` mutation even when the expected sum is zero +/// in other tests. +#[test] +fn balance_snapshot_total_nonzero_for_positive_balances() { + let mut map = std::collections::HashMap::new(); + map.insert(nssa::AccountId::new([42_u8; 32]), 1_u128); + let snap = BalanceSnapshot(map); + assert_ne!( + snap.total(), + 0, + "BalanceSnapshot::total must not return 0 when accounts have positive balances \ + (mutation: replaced with literal 0)" + ); +} + +/// Verifies that `StateIsolationOnFailure::name` returns a non-empty, non-"xyzzy" string. +#[test] +fn state_isolation_name_is_nonempty_and_not_placeholder() { + let inv = StateIsolationOnFailure; + let name = inv.name(); + assert!( + !name.is_empty(), + "StateIsolationOnFailure::name must not be empty" + ); + assert_ne!( + name, "xyzzy", + "StateIsolationOnFailure::name must not be 'xyzzy'" + ); + assert_eq!(name, "StateIsolationOnFailure"); +} + +/// Verifies that `BalanceConservation::name` returns a non-empty, non-"xyzzy" string. +#[test] +fn balance_conservation_name_is_nonempty_and_not_placeholder() { + let inv = BalanceConservation; + let name = inv.name(); + assert!( + !name.is_empty(), + "BalanceConservation::name must not be empty" + ); + assert_ne!( + name, "xyzzy", + "BalanceConservation::name must not be 'xyzzy'" + ); + assert_eq!(name, "BalanceConservation"); +} + +/// Verifies that `FailedTxNonceStability::name` returns a non-empty, non-"xyzzy" string. +#[test] +fn failed_tx_nonce_stability_name_is_nonempty_and_not_placeholder() { + let inv = FailedTxNonceStability; + let name = inv.name(); + assert!( + !name.is_empty(), + "FailedTxNonceStability::name must not be empty" + ); + assert_ne!( + name, "xyzzy", + "FailedTxNonceStability::name must not be 'xyzzy'" + ); + assert_eq!(name, "FailedTxNonceStability"); +} + +/// Verifies that `StateIsolationOnFailure::check` returns `Some` when execution failed and +/// the balance in `state_after` differs from `balances_before`. +#[test] +fn state_isolation_check_detects_balance_change_on_failure() { + let acc_id = nssa::AccountId::new([1_u8; 32]); + // State has balance 100 for acc_id. + let state = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0); + + // balances_before claims balance was 50, but state_after (== state) has 100. + let mut balances = std::collections::HashMap::new(); + balances.insert(acc_id, 50_u128); + + let ctx = InvariantCtx { + state_before: &state, + state_after: &state, + execution_succeeded: false, // failure → isolation invariant is active + balances_before: BalanceSnapshot(balances), + nonces_before: make_empty_nonce_snapshot(), + }; + + let inv = StateIsolationOnFailure; + let result = inv.check(&ctx); + assert!( + result.is_some(), + "StateIsolationOnFailure::check must return Some violation when \ + state_after balance (100) differs from balances_before (50) on a failed tx \ + (mutations: replace with None; delete !; replace != with ==)" + ); +} + +/// Verifies that `assert_replay_rejection` panics when the replayed transaction is +/// accepted (i.e. NOT rejected — a genuine invariant violation). +#[test] +fn assert_replay_rejection_panics_when_replay_not_rejected() { + let accounts = test_accounts(); + let (from_id, from_key) = &accounts[0]; + let (to_id, _) = &accounts[1]; + + // Build a state that contains the sender account with nonce 0 and sufficient balance. + let genesis: Vec<(nssa::AccountId, u128)> = accounts + .iter() + .map(|(id, _)| (*id, 10_000_000_u128)) + .collect(); + let mut state = V03State::new_with_genesis_accounts(&genesis, vec![], 0); + + // Create a valid, signed transaction with nonce 0 (the initial nonce in state). + let tx = common::test_utils::create_transaction_native_token_transfer( + *from_id, 0, *to_id, 100, from_key, + ); + + // We do NOT apply the tx first. The state nonce is still 0, so calling + // execute_check_on_state would SUCCEED — making this a "successful replay". + // assert_replay_rejection is supposed to panic here (INVARIANT VIOLATION [ReplayRejection]). + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + assert_replay_rejection(tx, &mut state, 0, 0); + })); + + assert!( + result.is_err(), + "assert_replay_rejection must panic when the replayed tx is accepted \ + (mutation: replace function body with () \u{2014} no-op skips the check)" + ); +} + +/// Verifies that `assert_tx_execution_invariants` is NOT a no-op by providing a +/// context that violates `StateIsolationOnFailure` and expecting a panic. +#[test] +fn assert_tx_execution_invariants_is_not_noop() { + let acc_id = nssa::AccountId::new([5_u8; 32]); + // Both state_before and state_after have the account at balance 100. + let state_before = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0); + let mut state_after = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0); + + // Lie: claim balance was 50 before. State_after shows 100. + // With execution_succeeded=false, StateIsolationOnFailure detects the discrepancy. + let mut balances = std::collections::HashMap::new(); + balances.insert(acc_id, 50_u128); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + assert_tx_execution_invariants( + &state_before, + &mut state_after, + BalanceSnapshot(balances), + make_empty_nonce_snapshot(), + Err::("simulated failure"), + (1, 1), + ); + })); + + assert!( + result.is_err(), + "assert_tx_execution_invariants must panic on a StateIsolationOnFailure violation \ + (mutation: replace entire function body with () \u{2014} no-op skips all invariant checks)" + ); +} From fd95df7c6f59bdb38b5734ae658e043f596cd20b Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 5 Jun 2026 18:55:53 +0800 Subject: [PATCH 10/31] fix: test in harness --- fuzz_props/src/tests/invariants.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fuzz_props/src/tests/invariants.rs b/fuzz_props/src/tests/invariants.rs index 1d799abd..18f79e6d 100644 --- a/fuzz_props/src/tests/invariants.rs +++ b/fuzz_props/src/tests/invariants.rs @@ -255,8 +255,10 @@ fn assert_replay_rejection_panics_when_replay_not_rejected() { // We do NOT apply the tx first. The state nonce is still 0, so calling // execute_check_on_state would SUCCEED — making this a "successful replay". // assert_replay_rejection is supposed to panic here (INVARIANT VIOLATION [ReplayRejection]). + // block_id=0 is the genesis block; transactions are only valid from block_id=1 onwards, + // so use (1, 0) to ensure execute_check_on_state accepts the tx (triggering the panic). let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - assert_replay_rejection(tx, &mut state, 0, 0); + assert_replay_rejection(tx, &mut state, 1, 0); })); assert!( From 1bb51acd8796d31833ffbd2ed106a5bb6c8ea893 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 6 Jun 2026 00:31:48 +0800 Subject: [PATCH 11/31] fix: make the test platform neutral --- fuzz_props/src/tests/invariants.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/fuzz_props/src/tests/invariants.rs b/fuzz_props/src/tests/invariants.rs index 18f79e6d..0d62fb36 100644 --- a/fuzz_props/src/tests/invariants.rs +++ b/fuzz_props/src/tests/invariants.rs @@ -240,25 +240,28 @@ fn assert_replay_rejection_panics_when_replay_not_rejected() { let (from_id, from_key) = &accounts[0]; let (to_id, _) = &accounts[1]; - // Build a state that contains the sender account with nonce 0 and sufficient balance. let genesis: Vec<(nssa::AccountId, u128)> = accounts .iter() .map(|(id, _)| (*id, 10_000_000_u128)) .collect(); - let mut state = V03State::new_with_genesis_accounts(&genesis, vec![], 0); - // Create a valid, signed transaction with nonce 0 (the initial nonce in state). let tx = common::test_utils::create_transaction_native_token_transfer( *from_id, 0, *to_id, 100, from_key, ); + let validated = tx + .transaction_stateless_check() + .expect("test setup: transaction must pass stateless validation"); + let mut scratch_state = V03State::new_with_genesis_accounts(&genesis, vec![], 0); + let applied_tx = validated + .execute_check_on_state(&mut scratch_state, 1, 1) + .expect("test setup: first execution must succeed (block_id=1, timestamp=1)"); - // We do NOT apply the tx first. The state nonce is still 0, so calling - // execute_check_on_state would SUCCEED — making this a "successful replay". - // assert_replay_rejection is supposed to panic here (INVARIANT VIOLATION [ReplayRejection]). - // block_id=0 is the genesis block; transactions are only valid from block_id=1 onwards, - // so use (1, 0) to ensure execute_check_on_state accepts the tx (triggering the panic). + // Replay `applied_tx` (nonce 0) against a FRESH state still at nonce 0. + // The nonce matches → execute_check_on_state ACCEPTS the replay — a protocol + // violation that assert_replay_rejection must detect and panic on. + let mut fresh_state = V03State::new_with_genesis_accounts(&genesis, vec![], 0); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - assert_replay_rejection(tx, &mut state, 1, 0); + assert_replay_rejection(applied_tx, &mut fresh_state, 1, 1); })); assert!( From 2b951f733b5683abda19c5c8b2841f2bc0917f73 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 6 Jun 2026 00:50:52 +0800 Subject: [PATCH 12/31] fix: add missing feature --- Cargo.lock | 790 ++++++++++++++++++++++++++++++++++++++++-- fuzz_props/Cargo.toml | 1 + 2 files changed, 769 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51a670b5..899caad6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,33 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addchain" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e33f6a175ec6a9e0aca777567f9ff7c3deefc255660df887e7fa3585e9801d8" +dependencies = [ + "num-bigint 0.3.3", + "num-integer", + "num-traits", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "cpp_demangle", + "fallible-iterator", + "gimli", + "memmap2", + "object", + "rustc-demangle", + "smallvec", + "typed-arena", +] + [[package]] name = "adler2" version = "2.0.1" @@ -209,7 +236,7 @@ dependencies = [ "fnv", "hashbrown 0.15.5", "itertools 0.13.0", - "num-bigint", + "num-bigint 0.4.6", "num-integer", "num-traits", "zeroize", @@ -228,7 +255,7 @@ dependencies = [ "derivative", "digest 0.10.7", "itertools 0.10.5", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "paste", "rustc_version", @@ -249,7 +276,7 @@ dependencies = [ "digest 0.10.7", "educe", "itertools 0.13.0", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "paste", "zeroize", @@ -281,7 +308,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "proc-macro2", "quote", @@ -294,7 +321,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "proc-macro2", "quote", @@ -370,7 +397,7 @@ dependencies = [ "ark-relations 0.5.1", "ark-std 0.5.0", "educe", - "num-bigint", + "num-bigint 0.4.6", "num-integer", "num-traits", "tracing", @@ -409,7 +436,7 @@ dependencies = [ "ark-serialize-derive 0.4.2", "ark-std 0.4.0", "digest 0.10.7", - "num-bigint", + "num-bigint 0.4.6", ] [[package]] @@ -422,7 +449,7 @@ dependencies = [ "ark-std 0.5.0", "arrayvec", "digest 0.10.7", - "num-bigint", + "num-bigint 0.4.6", ] [[package]] @@ -651,6 +678,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -852,6 +888,18 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -1015,6 +1063,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -1201,6 +1251,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1228,6 +1287,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1237,6 +1302,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -1463,7 +1538,7 @@ dependencies = [ "asn1-rs", "displaydoc", "nom 7.1.3", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "rusticata-macros", ] @@ -1575,6 +1650,15 @@ dependencies = [ "crypto-common 0.2.1", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1630,6 +1714,20 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "downloader" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac1e888d6830712d565b2f3a974be3200be9296bc1b03db8251a4cbf18a4a34" +dependencies = [ + "digest 0.10.7", + "futures", + "rand 0.8.5", + "reqwest", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "dtoa" version = "1.0.11" @@ -1761,6 +1859,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -1787,6 +1905,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -1818,6 +1947,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.4.1" @@ -1849,10 +1984,28 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ + "bitvec", + "byteorder", + "ff_derive", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "ff_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60" +dependencies = [ + "addchain", + "num-bigint 0.3.3", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1933,6 +2086,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.32" @@ -2071,6 +2230,30 @@ dependencies = [ "testnet_initial_state", ] +[[package]] +name = "gdbstub" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bafc7e33650ab9f05dcc16325f05d56b8d10393114e31a19a353b86fa60cfe7" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "log", + "managed", + "num-traits", + "pastey", +] + +[[package]] +name = "gdbstub_arch" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c02bfe7bd65f42bcda751456869dfa1eb2bd1c36e309b9ec27f4888d41cf258" +dependencies = [ + "gdbstub", + "num-traits", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2144,6 +2327,23 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap 2.14.0", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "group" version = "0.13.0" @@ -2174,6 +2374,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2233,6 +2442,20 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -2761,6 +2984,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "ipconfig" version = "0.3.4" @@ -2799,6 +3031,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -2848,6 +3089,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.95" @@ -3008,6 +3259,26 @@ version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +[[package]] +name = "liblzma" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6033b77c21d1f56deeae8014eb9fbe7bdf1765185a6c508b5ca82eeaed7f899" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a60851d15cd8c5346eca4ab8babff585be2ae4bc8097c067291d3ffe2add3b6" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libm" version = "0.2.16" @@ -3428,7 +3699,7 @@ checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862" dependencies = [ "ark-bn254 0.5.0", "ark-ff 0.5.0", - "num-bigint", + "num-bigint 0.4.6", "thiserror 1.0.69", ] @@ -3511,7 +3782,7 @@ dependencies = [ "logos-blockchain-poq", "logos-blockchain-poseidon2", "logos-blockchain-utils", - "num-bigint", + "num-bigint 0.4.6", "serde", "thiserror 2.0.18", "zeroize", @@ -3697,7 +3968,7 @@ dependencies = [ "logos-blockchain-utxotree", "multiaddr", "nom 8.0.0", - "num-bigint", + "num-bigint 0.4.6", "rpds", "serde", "strum", @@ -3752,7 +4023,7 @@ dependencies = [ "ark-serialize 0.4.2", "generic-array 1.3.5", "hex", - "num-bigint", + "num-bigint 0.4.6", "serde", "serde_json", "thiserror 2.0.18", @@ -3793,7 +4064,7 @@ dependencies = [ "logos-blockchain-poseidon2", "logos-blockchain-utils", "logos-blockchain-zksign", - "num-bigint", + "num-bigint 0.4.6", "rand_core 0.6.4", "serde", "subtle", @@ -3831,7 +4102,7 @@ dependencies = [ "logos-blockchain-pol", "logos-blockchain-utils", "logos-blockchain-utxotree", - "num-bigint", + "num-bigint 0.4.6", "rand 0.8.5", "rpds", "serde", @@ -3932,7 +4203,7 @@ dependencies = [ "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-proofs-error", - "num-bigint", + "num-bigint 0.4.6", "serde", "serde_json", "tracing", @@ -3951,7 +4222,7 @@ dependencies = [ "logos-blockchain-groth16", "logos-blockchain-proofs-error", "logos-blockchain-utils", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "serde", "serde_json", @@ -3970,7 +4241,7 @@ dependencies = [ "logos-blockchain-groth16", "logos-blockchain-pol", "logos-blockchain-proofs-error", - "num-bigint", + "num-bigint 0.4.6", "serde", "serde_json", "thiserror 2.0.18", @@ -3985,7 +4256,7 @@ dependencies = [ "ark-bn254 0.4.0", "ark-ff 0.4.2", "jf-poseidon2", - "num-bigint", + "num-bigint 0.4.6", ] [[package]] @@ -4109,7 +4380,7 @@ dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", "logos-blockchain-poseidon2", - "num-bigint", + "num-bigint 0.4.6", "rpds", "serde", "thiserror 2.0.18", @@ -4127,7 +4398,7 @@ dependencies = [ "logos-blockchain-groth16", "logos-blockchain-poseidon2", "logos-blockchain-proofs-error", - "num-bigint", + "num-bigint 0.4.6", "serde", "serde_json", "thiserror 2.0.18", @@ -4159,6 +4430,64 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "malachite" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbdf9cb251732db30a7200ebb6ae5d22fe8e11397364416617d2c2cf0c51cb5" +dependencies = [ + "malachite-base", + "malachite-float", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-base" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea0ed76adf7defc1a92240b5c36d5368cfe9251640dcce5bd2d0b7c1fd87aeb" +dependencies = [ + "hashbrown 0.14.5", + "itertools 0.11.0", + "libm", + "ryu", +] + +[[package]] +name = "malachite-float" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9d20db1c73759c1377db7b27575df6f2eab7368809dd62c0a715dc1bcc39f7" +dependencies = [ + "itertools 0.11.0", + "malachite-base", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-nz" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34a79feebb2bc9aa7762047c8e5495269a367da6b5a90a99882a0aeeac1841f7" +dependencies = [ + "itertools 0.11.0", + "libm", + "malachite-base", +] + +[[package]] +name = "malachite-q" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f235d5747b1256b47620f5640c2a17a88c7569eebdf27cd9cb130e1a619191" +dependencies = [ + "itertools 0.11.0", + "malachite-base", + "malachite-nz", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -4168,6 +4497,12 @@ dependencies = [ "libc", ] +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "match-lookup" version = "0.1.2" @@ -4200,12 +4535,31 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "merlin" version = "3.0.0" @@ -4376,6 +4730,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", + "rayon", +] + [[package]] name = "netdev" version = "0.31.0" @@ -4542,6 +4912,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -4568,12 +4949,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -4625,6 +5026,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "nvtx" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2e855e8019f99e4b94ac33670eb4e4f570a2e044f3749a0b2c7f83b841e52c" +dependencies = [ + "cc", +] + [[package]] name = "objc" version = "0.2.7" @@ -4634,6 +5044,17 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "flate2", + "memchr", + "ruzstd", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -4815,6 +5236,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pem" version = "3.0.6" @@ -4897,6 +5324,12 @@ dependencies = [ "spki 0.8.0", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "polling" version = "3.11.0" @@ -4929,6 +5362,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postcard" version = "1.1.3" @@ -4938,6 +5380,7 @@ dependencies = [ "cobs", "embedded-io 0.4.0", "embedded-io 0.6.1", + "heapless", "serde", ] @@ -5136,6 +5579,20 @@ dependencies = [ "prost 0.13.5", ] +[[package]] +name = "puffin" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f" +dependencies = [ + "anyhow", + "byteorder", + "cfg-if", + "itertools 0.10.5", + "once_cell", + "parking_lot", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -5241,6 +5698,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -5315,6 +5778,32 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rcgen" version = "0.13.2" @@ -5469,6 +5958,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ringbuffer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53" + [[package]] name = "risc0-binfmt" version = "3.0.4" @@ -5515,6 +6010,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "risc0-build-kernel" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaaa3e04c71e4244354dd9e3f8b89378cfecfbb03f9c72de4e2e7e0482b30c9a" +dependencies = [ + "cc", + "directories", + "hex", + "rayon", + "sha2", + "tempfile", +] + [[package]] name = "risc0-circuit-keccak" version = "4.0.5" @@ -5523,14 +6032,34 @@ checksum = "5f543c60287fece797a5da4209384ab1bfebd9644fcfe591e11b1aa85f1a02f8" dependencies = [ "anyhow", "bytemuck", + "cfg-if", + "keccak 0.1.6", + "liblzma", "paste", + "rayon", "risc0-binfmt", + "risc0-circuit-keccak-sys", "risc0-circuit-recursion", "risc0-core", + "risc0-sys", "risc0-zkp", "tracing", ] +[[package]] +name = "risc0-circuit-keccak-sys" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eae53a7bf1c09828dfd46ed5c942cefbf4bef3c4400f6758001569a834c462" +dependencies = [ + "cc", + "derive_more", + "glob", + "risc0-build-kernel", + "risc0-core", + "risc0-sys", +] + [[package]] name = "risc0-circuit-recursion" version = "4.0.4" @@ -5539,11 +6068,33 @@ checksum = "2347e909c6b2a65584b5898f3802eec5b8c1b4b45329edfdd8587b6a04dd3357" dependencies = [ "anyhow", "bytemuck", + "cfg-if", + "downloader", "hex", + "lazy-regex", "metal", + "rand 0.9.3", + "rayon", + "risc0-circuit-recursion-sys", "risc0-core", + "risc0-sys", "risc0-zkp", + "serde", + "sha2", "tracing", + "zip", +] + +[[package]] +name = "risc0-circuit-recursion-sys" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132be7ccca4b39ec957cc37d5083ca2aee171407922352002c9812452a890b39" +dependencies = [ + "glob", + "risc0-build-kernel", + "risc0-core", + "risc0-sys", ] [[package]] @@ -5555,13 +6106,43 @@ dependencies = [ "anyhow", "bit-vec", "bytemuck", + "byteorder", + "cfg-if", "derive_more", + "enum-map", + "gdbstub", + "gdbstub_arch", + "malachite", + "num-derive", + "num-traits", "paste", + "postcard", + "rand 0.9.3", + "rayon", + "ringbuffer", "risc0-binfmt", + "risc0-circuit-rv32im-sys", "risc0-core", + "risc0-sys", "risc0-zkp", "serde", + "smallvec", "tracing", + "wide", +] + +[[package]] +name = "risc0-circuit-rv32im-sys" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d677ec41e475534e18e58889ef0626dcdabf5e918804ef847da0c0bbf300b3" +dependencies = [ + "cc", + "derive_more", + "glob", + "risc0-build-kernel", + "risc0-core", + "risc0-sys", ] [[package]] @@ -5571,6 +6152,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b956a976b8ce4713694dcc6c370b522a42ccef4ba45da5b6e57dbf26cdb7b1" dependencies = [ "bytemuck", + "nvtx", + "puffin", "rand_core 0.9.5", ] @@ -5587,12 +6170,28 @@ dependencies = [ "ark-groth16 0.5.0", "ark-serialize 0.5.0", "bytemuck", + "cfg-if", "hex", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "risc0-binfmt", + "risc0-core", "risc0-zkp", + "rzup", "serde", + "serde_json", + "tempfile", + "tracing", +] + +[[package]] +name = "risc0-sys" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960c8295fbb87e1e73e332f8f7de2fba0252377575042d9d3e9a4eb50a38e078" +dependencies = [ + "anyhow", + "risc0-build-kernel", ] [[package]] @@ -5618,12 +6217,18 @@ dependencies = [ "bytemuck", "cfg-if", "digest 0.10.7", + "ff", "hex", "hex-literal", "metal", + "ndarray", + "parking_lot", "paste", + "rand 0.9.3", "rand_core 0.9.5", + "rayon", "risc0-core", + "risc0-sys", "risc0-zkvm-platform", "serde", "sha2", @@ -5637,15 +6242,27 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22b7eafb5d85be59cbd9da83f662cf47d834f1b836e14f675d1530b12c666867" dependencies = [ + "addr2line", "anyhow", "bincode", "borsh", "bytemuck", "bytes", "derive_more", + "elf", + "enum-map", + "gdbstub", + "gdbstub_arch", + "gimli", "hex", + "keccak 0.1.6", "lazy-regex", + "num-bigint 0.4.6", + "num-traits", + "object", "prost 0.13.5", + "rand 0.9.3", + "rayon", "risc0-binfmt", "risc0-build", "risc0-circuit-keccak", @@ -5657,6 +6274,7 @@ dependencies = [ "risc0-zkp", "risc0-zkvm-platform", "rrs-lib", + "rustc-demangle", "rzup", "semver", "serde", @@ -5664,6 +6282,7 @@ dependencies = [ "stability", "tempfile", "tracing", + "typetag", ] [[package]] @@ -5781,6 +6400,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -5872,6 +6497,15 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "ruzstd" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" +dependencies = [ + "twox-hash", +] + [[package]] name = "rw-stream-sink" version = "0.4.0" @@ -5908,6 +6542,15 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "schemars" version = "0.9.0" @@ -6225,6 +6868,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -6387,6 +7033,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.46" @@ -6927,12 +7579,58 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "typetag" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a897b12c6c1151ad0b138b8db50252dc301f93bc3b027db05eec82aeed298c" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf808357c6ed7e13ba0f3277ec8d8f21b2d501274895104263985330c726c1c5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "uint" version = "0.10.0" @@ -7316,6 +8014,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "widestring" version = "1.2.1" @@ -7733,6 +8441,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -7924,8 +8641,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.14.0", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/fuzz_props/Cargo.toml b/fuzz_props/Cargo.toml index a6796bd0..7e6ccc78 100644 --- a/fuzz_props/Cargo.toml +++ b/fuzz_props/Cargo.toml @@ -21,3 +21,4 @@ testnet_initial_state = { workspace = true } [dev-dependencies] proptest = "1.4" +nssa = { workspace = true, features = ["prove"] } From 9d18c9f3468b48a9b0a4b2e349565ab3c47600bf Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 7 Jun 2026 08:31:09 +0800 Subject: [PATCH 13/31] fix: update mutants-protocol recipe --- Justfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Justfile b/Justfile index e552881a..949f9131 100644 --- a/Justfile +++ b/Justfile @@ -654,7 +654,7 @@ coverage-all ENGINE="all": # Oracle: cargo test -p fuzz_props --release # Run on every PR that touches fuzz_props/ or fuzz/fuzz_targets/. # -# Plane B (slow, ~hours): mutates LEZ protocol code (nssa, common). +# Plane B (slow, ~hours): mutates LEZ protocol code (lee, common). # Oracle: all 15 fuzz targets replayed against their committed corpus. # Run weekly or manually to find corpus gaps. @@ -677,7 +677,7 @@ mutants-harness: # Plane B — mutation testing of the LEZ protocol code against the committed corpus. # -# Mutates nssa and common in the logos-execution-zone sibling workspace and uses +# Mutates lee and common in the logos-execution-zone sibling workspace and uses # scripts/mutants-corpus-test.sh as the oracle. The oracle replays all 15 # committed libFuzzer corpora (cargo fuzz run -runs=0) against each mutant. # @@ -694,7 +694,7 @@ mutants-harness: # Default covers the two highest-value protocol crates. # # Output report: mutants-protocol.out/ in the repository root. -mutants-protocol PACKAGES="nssa common": +mutants-protocol PACKAGES="lee common": #!/bin/bash set -euo pipefail REPO_DIR="$(pwd)" From 97cdf142fe99201c9c92d96b8877abfb2636f0a5 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 8 Jun 2026 10:35:30 +0800 Subject: [PATCH 14/31] fix: remove pre-lee regressions --- .../fuzz_encoding_roundtrip/regression_0001 | 0 .../regression_0002 | 1 - .../libfuzz/fuzz_state_transition/regression_0003 | 0 .../fuzz_witness_set_verification/regression_0004 | Bin 71 -> 0 bytes .../fuzz_witness_set_verification/regression_0005 | Bin 70 -> 0 bytes 5 files changed, 1 deletion(-) delete mode 100644 corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 delete mode 100644 corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 delete mode 100644 corpus/libfuzz/fuzz_state_transition/regression_0003 delete mode 100644 corpus/libfuzz/fuzz_witness_set_verification/regression_0004 delete mode 100644 corpus/libfuzz/fuzz_witness_set_verification/regression_0005 diff --git a/corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 b/corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 deleted file mode 100644 index e69de29b..00000000 diff --git a/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 b/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 deleted file mode 100644 index 8c7e5a66..00000000 --- a/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 +++ /dev/null @@ -1 +0,0 @@ -A \ No newline at end of file diff --git a/corpus/libfuzz/fuzz_state_transition/regression_0003 b/corpus/libfuzz/fuzz_state_transition/regression_0003 deleted file mode 100644 index e69de29b..00000000 diff --git a/corpus/libfuzz/fuzz_witness_set_verification/regression_0004 b/corpus/libfuzz/fuzz_witness_set_verification/regression_0004 deleted file mode 100644 index 327f06de5ea4130275f753c8d3b553d59f00379e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71 qcmZ=cG<`Y)2yhsHDG)JzdeU@8I1eJSefsq2BB*jKKp}`;hUoyv(+zR} diff --git a/corpus/libfuzz/fuzz_witness_set_verification/regression_0005 b/corpus/libfuzz/fuzz_witness_set_verification/regression_0005 deleted file mode 100644 index a5d82fa63689e4d9f5cd8cd0d01c97ff240ee8ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70 pcmZ=cG<`Y)1A{1tfB<$F1DzHDsuG!=G@TJ5!7_a+SSye>9RLm<2toh= From 7ae3e40661f603294253d2aa05c97ac1c8dd97b2 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 8 Jun 2026 10:44:25 +0800 Subject: [PATCH 15/31] chore: add post-lee regressions to the corpus --- .../fuzz_encoding_roundtrip/regression_0001 | 0 .../regression_0002 | 1 + .../libfuzz/fuzz_state_transition/regression_0003 | 0 .../fuzz_witness_set_verification/regression_0004 | Bin 0 -> 71 bytes .../fuzz_witness_set_verification/regression_0005 | Bin 0 -> 70 bytes .../fuzz_witness_set_verification/regression_0006 | Bin 0 -> 71 bytes 6 files changed, 1 insertion(+) create mode 100644 corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 create mode 100644 corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 create mode 100644 corpus/libfuzz/fuzz_state_transition/regression_0003 create mode 100644 corpus/libfuzz/fuzz_witness_set_verification/regression_0004 create mode 100644 corpus/libfuzz/fuzz_witness_set_verification/regression_0005 create mode 100644 corpus/libfuzz/fuzz_witness_set_verification/regression_0006 diff --git a/corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 b/corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 new file mode 100644 index 00000000..e69de29b diff --git a/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 b/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 new file mode 100644 index 00000000..3a6e607a --- /dev/null +++ b/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/corpus/libfuzz/fuzz_state_transition/regression_0003 b/corpus/libfuzz/fuzz_state_transition/regression_0003 new file mode 100644 index 00000000..e69de29b diff --git a/corpus/libfuzz/fuzz_witness_set_verification/regression_0004 b/corpus/libfuzz/fuzz_witness_set_verification/regression_0004 new file mode 100644 index 0000000000000000000000000000000000000000..327f06de5ea4130275f753c8d3b553d59f00379e GIT binary patch literal 71 qcmZ=cG<`Y)2yhsHDG)JzdeU@8I1eJSefsq2BB*jKKp}`;hUoyv(+zR} literal 0 HcmV?d00001 diff --git a/corpus/libfuzz/fuzz_witness_set_verification/regression_0005 b/corpus/libfuzz/fuzz_witness_set_verification/regression_0005 new file mode 100644 index 0000000000000000000000000000000000000000..a5d82fa63689e4d9f5cd8cd0d01c97ff240ee8ce GIT binary patch literal 70 pcmZ=cG<`Y)1A{1tfB<$F1DzHDsuG!=G@TJ5!7_a+SSye>9RLm<2toh= literal 0 HcmV?d00001 diff --git a/corpus/libfuzz/fuzz_witness_set_verification/regression_0006 b/corpus/libfuzz/fuzz_witness_set_verification/regression_0006 new file mode 100644 index 0000000000000000000000000000000000000000..52668851a87c57a54730564f923e903b11ac6444 GIT binary patch literal 71 gcmZRuW&nc*hX2<2R>6!64Aa2;-hPNUQ6x(T00%AwF#rGn literal 0 HcmV?d00001 From 1cfe58ebf9aa35d0fc7010b06abc8b8cd4e84ada Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 8 Jun 2026 15:37:01 +0800 Subject: [PATCH 16/31] fix: remove stale artifacts - regression recipe for per target --- Justfile | 15 ++++++++++++--- .../fuzz_encoding_roundtrip/regression_0001 | 0 .../regression_0002 | 1 - .../libfuzz/fuzz_state_transition/regression_0003 | 0 4 files changed, 12 insertions(+), 4 deletions(-) delete mode 100644 corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 delete mode 100644 corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 delete mode 100644 corpus/libfuzz/fuzz_state_transition/regression_0003 diff --git a/Justfile b/Justfile index 949f9131..947f9896 100644 --- a/Justfile +++ b/Justfile @@ -39,11 +39,20 @@ fuzz TIME="30": cargo fuzz run "$target" "corpus/libfuzz/$target" -- -max_total_time={{TIME}} done -# Re-run the saved corpus for every target (regression mode, no new mutations) -fuzz-regression: +# Re-run the saved corpus for one or ALL targets (regression mode, no new mutations). +# When TARGET is omitted every registered target is replayed in sequence. +# Usage: just fuzz-regression # all targets +# just fuzz-regression fuzz_state_transition # single target +fuzz-regression TARGET="": #!/bin/bash set -euo pipefail - for target in $(cargo fuzz list 2>/dev/null); do + TARGET="{{TARGET}}" + if [ -z "$TARGET" ]; then + TARGETS=($(cargo fuzz list 2>/dev/null)) + else + TARGETS=("$TARGET") + fi + for target in "${TARGETS[@]}"; do echo "=== regression $target ===" mkdir -p "corpus/libfuzz/$target" cargo fuzz run "$target" "corpus/libfuzz/$target" -- -runs=0 diff --git a/corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 b/corpus/libfuzz/fuzz_encoding_roundtrip/regression_0001 deleted file mode 100644 index e69de29b..00000000 diff --git a/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 b/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 deleted file mode 100644 index 3a6e607a..00000000 --- a/corpus/libfuzz/fuzz_multi_block_state_sequence/regression_0002 +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/corpus/libfuzz/fuzz_state_transition/regression_0003 b/corpus/libfuzz/fuzz_state_transition/regression_0003 deleted file mode 100644 index e69de29b..00000000 From 2974cd5e30e428477581f893fbc3d6c88d9c78af Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 9 Jun 2026 11:51:39 +0800 Subject: [PATCH 17/31] test: add merkle tree target to catch 33 mutants --- fuzz/Cargo.lock | 1 + fuzz/Cargo.toml | 7 ++ fuzz/fuzz_targets/fuzz_merkle_tree.rs | 130 ++++++++++++++++++++++++++ scripts/mutants-corpus-test.sh | 1 + 4 files changed, 139 insertions(+) create mode 100644 fuzz/fuzz_targets/fuzz_merkle_tree.rs diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 413470e0..3d8a3101 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -2084,6 +2084,7 @@ dependencies = [ "lee", "lee_core", "libfuzzer-sys", + "sha2", "testnet_initial_state", ] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index de3cb487..47b98bb4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -44,6 +44,7 @@ libfuzzer-sys = { version = "0.4", optional = true } afl = { version = "0.15", optional = true } arbitrary = { version = "1", features = ["derive"] } borsh = "1" +sha2 = "0.10" nssa = { path = "../../logos-execution-zone/lee/state_machine", package = "lee" } nssa_core = { path = "../../logos-execution-zone/lee/state_machine/core", package = "lee_core" } common = { path = "../../logos-execution-zone/lez/common" } @@ -119,3 +120,9 @@ name = "fuzz_sequencer_vs_replayer" path = "fuzz_targets/fuzz_sequencer_vs_replayer.rs" test = false bench = false + +[[bin]] +name = "fuzz_merkle_tree" +path = "fuzz_targets/fuzz_merkle_tree.rs" +test = false +bench = false diff --git a/fuzz/fuzz_targets/fuzz_merkle_tree.rs b/fuzz/fuzz_targets/fuzz_merkle_tree.rs new file mode 100644 index 00000000..a88249b6 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_merkle_tree.rs @@ -0,0 +1,130 @@ +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +//! Fuzz target: `MerkleTree` structural invariants +//! +//! Covered code paths (all in `lee/state_machine/src/merkle_tree/mod.rs`): +//! +//! ```text +//! MerkleTree::with_capacity(1) ← initial capacity forces reallocate_to_double_capacity +//! MerkleTree::insert(value) ← per-value; also triggers reallocate_to_double_capacity +//! MerkleTree::root() ← sampled once after all inserts +//! MerkleTree::get_authentication_path_for(index) ← per-value +//! prev_power_of_two ← exercised inside reallocate_to_double_capacity +//! ``` +//! +//! # Input format +//! +//! The raw fuzz bytes are sliced into 32-byte chunks; each chunk becomes one +//! value inserted into the tree. This makes the format trivial to reason about +//! and lets us seed the corpus with well-known test vectors. +//! +//! # Invariants checked +//! +//! 1. **InsertionIndex** — `insert(value)` returns the sequential 0-based index. +//! 2. **AuthPathSome** — `get_authentication_path_for(i)` is `Some` for every +//! `i < length`. +//! 3. **AuthPathValid** — every returned path re-hashes (SHA-256, same hash +//! functions used by the production code) to the value reported by `root()`. +//! 4. **OutOfBoundsNone** — `get_authentication_path_for(length)` returns `None`. + +use sha2::{Digest as _, Sha256}; + +// ─── Reference hash helpers (mirrors the private functions in merkle_tree/mod.rs) ─── + +/// SHA-256 of a single 32-byte leaf value. Mirrors `hash_value`. +fn sha256_one(v: &[u8; 32]) -> [u8; 32] { + let mut h = Sha256::new(); + h.update(v); + h.finalize().into() +} + +/// SHA-256 of two concatenated 32-byte nodes. Mirrors `hash_two`. +fn sha256_two(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { + let mut h = Sha256::new(); + h.update(left); + h.update(right); + h.finalize().into() +} + +/// Reference implementation of authentication-path verification. +/// +/// Mirrors `verify_authentication_path` from the test module inside +/// `lee/state_machine/src/merkle_tree/mod.rs`. +/// +/// Algorithm: +/// result ← SHA-256(value) +/// for each sibling in path: +/// if level_index is even → result is the LEFT child → hash(result, sibling) +/// if level_index is odd → result is the RIGHT child → hash(sibling, result) +/// level_index >>= 1 +/// return result == root +fn verify_auth_path(value: &[u8; 32], index: usize, path: &[[u8; 32]], root: &[u8; 32]) -> bool { + let mut result = sha256_one(value); + let mut level_index = index; + for sibling in path { + let is_left_child = level_index & 1 == 0; + result = if is_left_child { + sha256_two(&result, sibling) + } else { + sha256_two(sibling, &result) + }; + level_index >>= 1; + } + &result == root +} + +fuzz_props::fuzz_entry!(|data: &[u8]| { + // Treat each 32-byte chunk as one leaf value. Discard any trailing + // incomplete chunk. + let values: Vec<[u8; 32]> = data + .chunks_exact(32) + .map(|c| c.try_into().expect("chunks_exact(32) always yields [u8;32]")) + .collect(); + + // Nothing to test with an empty input. + if values.is_empty() { + return; + } + + // Start with capacity=1 so the very first pair of insertions triggers + // `reallocate_to_double_capacity`, and each subsequent power-of-two boundary + // triggers it again. This exercises `prev_power_of_two`, the copy loop, + // and the capacity / length bookkeeping inside the reallocation path. + let mut tree = nssa::merkle_tree::MerkleTree::with_capacity(1); + + // ── INVARIANT [InsertionIndex] ──────────────────────────────────────────── + // insert() must return 0, 1, 2, … in order. + for (expected_index, &value) in values.iter().enumerate() { + let actual_index = tree.insert(value); + assert_eq!( + actual_index, + expected_index, + "INVARIANT VIOLATION [InsertionIndex]: \ + insert returned {actual_index} but expected {expected_index}", + ); + } + + let root = tree.root(); + + // ── INVARIANTS [AuthPathSome] and [AuthPathValid] ───────────────────────── + for (index, value) in values.iter().enumerate() { + let path = tree + .get_authentication_path_for(index) + .expect("INVARIANT VIOLATION [AuthPathSome]: \ + get_authentication_path_for returned None for a valid index"); + + assert!( + verify_auth_path(value, index, &path, &root), + "INVARIANT VIOLATION [AuthPathValid]: \ + authentication path for index {index} does not re-hash to root()", + ); + } + + // ── INVARIANT [OutOfBoundsNone] ─────────────────────────────────────────── + // The index one past the last inserted element must yield None. + assert!( + tree.get_authentication_path_for(values.len()).is_none(), + "INVARIANT VIOLATION [OutOfBoundsNone]: \ + get_authentication_path_for({}) should return None but returned Some", + values.len(), + ); +}); diff --git a/scripts/mutants-corpus-test.sh b/scripts/mutants-corpus-test.sh index cd95d555..7ad68301 100755 --- a/scripts/mutants-corpus-test.sh +++ b/scripts/mutants-corpus-test.sh @@ -36,6 +36,7 @@ targets=( fuzz_apply_state_diff_split_path fuzz_multi_block_state_sequence fuzz_sequencer_vs_replayer + fuzz_merkle_tree ) # cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.). From cf24be46d989bd1a9be86eb8879493b20956eeb1 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 9 Jun 2026 14:09:11 +0800 Subject: [PATCH 18/31] test: add genesis invariants target to catch 8 mutants --- .gitignore | 1 + corpus/libfuzz/fuzz_genesis_invariants/seed | Bin 0 -> 1 bytes fuzz/Cargo.toml | 6 + fuzz/fuzz_targets/fuzz_genesis_invariants.rs | 137 +++++++++++++++++++ scripts/mutants-corpus-test.sh | 1 + 5 files changed, 145 insertions(+) create mode 100644 corpus/libfuzz/fuzz_genesis_invariants/seed create mode 100644 fuzz/fuzz_targets/fuzz_genesis_invariants.rs diff --git a/.gitignore b/.gitignore index 48506a63..62affb83 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ fuzz/coverage/ mutants.out/ mutants-harness.out/ mutants-protocol.out/ +mutants-protocol.out.backup/ # ── Misc ────────────────────────────────────────────────────────────────────── # Performance baseline output from `just perf-baseline` or CI diff --git a/corpus/libfuzz/fuzz_genesis_invariants/seed b/corpus/libfuzz/fuzz_genesis_invariants/seed new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 47b98bb4..762b496e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -126,3 +126,9 @@ name = "fuzz_merkle_tree" path = "fuzz_targets/fuzz_merkle_tree.rs" test = false bench = false + +[[bin]] +name = "fuzz_genesis_invariants" +path = "fuzz_targets/fuzz_genesis_invariants.rs" +test = false +bench = false diff --git a/fuzz/fuzz_targets/fuzz_genesis_invariants.rs b/fuzz/fuzz_targets/fuzz_genesis_invariants.rs new file mode 100644 index 00000000..fafb53b3 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_genesis_invariants.rs @@ -0,0 +1,137 @@ +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +//! Fuzz target: genesis-state and system-account invariants. +//! +//! This target is **input-independent**: the fuzz input is always ignored. +//! It asserts deterministic invariants about the genesis state produced by +//! `V03State::new_with_genesis_accounts`, `system_faucet_account_id`, +//! `system_bridge_account_id`, and `V03State::add_pinata_token_program`. +//! +//! # Covered mutations (from `lee/state_machine/src/state.rs`) +//! +//! | Line | Mutation | Assertion that catches it | +//! |------|--------------------------------------------------------|-----------------------------------------------------| +//! | 312 | `commitment_set_digest → Default::default()` | `[CommitmentSetDigestNonDefault]` | +//! | 368 | delete `program_owner` from `add_pinata_token_program` | `[PinataTokenProgramOwner]` | +//! | 370 | delete `data` from `add_pinata_token_program` | `[PinataTokenData]` | +//! | 385 | `system_faucet_account → Default::default()` | `[FaucetBalance]` + `[FaucetProgramOwner]` | +//! | 386 | delete `program_owner` from `system_faucet_account` | `[FaucetProgramOwner]` | +//! | 387 | delete `balance` from `system_faucet_account` | `[FaucetBalance]` | +//! | 393 | `system_bridge_account → Default::default()` | `[BridgeProgramOwner]` | +//! | 394 | delete `program_owner` from `system_bridge_account` | `[BridgeProgramOwner]` | +//! | 406 | `system_bridge_account_id → Default::default()` | `[BridgeIdNonDefault]` + `[SystemAccountIdDistinct]` | +//! +//! # Corpus note +//! +//! A single `\x00` seed file is sufficient — the input bytes are never read. +//! The seed is required by `cargo fuzz run -runs=0` so that the replay phase +//! has at least one execution to check against. + +use nssa::{Account, AccountId, V03State, system_bridge_account_id, system_faucet_account_id}; + +fuzz_props::fuzz_entry!(|_data: &[u8]| { + let default_account = Account::default(); + + // ── INVARIANT [BridgeIdNonDefault] ──────────────────────────────────────── + // `system_bridge_account_id()` must return a non-default `AccountId`. + // Catches the mutation at state.rs:406 that replaces the function body with + // `Default::default()`. + let bridge_id = system_bridge_account_id(); + assert_ne!( + bridge_id, + AccountId::default(), + "INVARIANT VIOLATION [BridgeIdNonDefault]: \ + system_bridge_account_id() must not return AccountId::default()", + ); + + // The two system account IDs must also be distinct so that they occupy + // separate entries in the public-state map. + let faucet_id = system_faucet_account_id(); + assert_ne!( + faucet_id, + bridge_id, + "INVARIANT VIOLATION [SystemAccountIdDistinct]: \ + system_faucet_account_id() and system_bridge_account_id() must differ", + ); + + // Build the genesis state with no extra accounts. + let state = V03State::new_with_genesis_accounts(&[], vec![], 0); + + // ── INVARIANT [FaucetBalance] ───────────────────────────────────────────── + // The system faucet account must hold `u128::MAX` tokens. + // Catches state.rs:385 (whole account → Default) and + // state.rs:387 (delete `balance` field from struct literal). + let faucet = state.get_account_by_id(faucet_id); + assert_eq!( + faucet.balance, + u128::MAX, + "INVARIANT VIOLATION [FaucetBalance]: \ + system_faucet_account must have balance == u128::MAX, got {}", + faucet.balance, + ); + + // ── INVARIANT [FaucetProgramOwner] ──────────────────────────────────────── + // The system faucet account must have a non-default `program_owner`. + // Catches state.rs:385 (whole account → Default) and + // state.rs:386 (delete `program_owner` field from struct literal). + assert_ne!( + faucet.program_owner, + default_account.program_owner, + "INVARIANT VIOLATION [FaucetProgramOwner]: \ + system_faucet_account must have a non-default program_owner", + ); + + // ── INVARIANT [BridgeProgramOwner] ─────────────────────────────────────── + // The system bridge account must have a non-default `program_owner`. + // Catches state.rs:393 (whole account → Default) and + // state.rs:394 (delete `program_owner` field from struct literal). + let bridge = state.get_account_by_id(bridge_id); + assert_ne!( + bridge.program_owner, + default_account.program_owner, + "INVARIANT VIOLATION [BridgeProgramOwner]: \ + system_bridge_account must have a non-default program_owner", + ); + + // ── INVARIANT [CommitmentSetDigestNonDefault] ───────────────────────────── + // A freshly created empty state has an all-zero Merkle root, which equals + // `CommitmentSetDigest::default()`. The genesis state inserts + // `DUMMY_COMMITMENT` via SHA-256, producing a strictly different root. + // Catches state.rs:312 that replaces `commitment_set_digest()` with + // `Default::default()`. + let empty_digest = V03State::new().commitment_set_digest(); + let genesis_digest = state.commitment_set_digest(); + assert_ne!( + genesis_digest, + empty_digest, + "INVARIANT VIOLATION [CommitmentSetDigestNonDefault]: \ + commitment_set_digest of genesis state must differ from the empty state's \ + all-zero root", + ); + + // ── INVARIANT [PinataTokenProgramOwner] ────────────────────────────────── + // An account created by `add_pinata_token_program` must have a non-default + // `program_owner` field. + // Catches state.rs:368 (delete `program_owner` from the struct literal). + // + // ── INVARIANT [PinataTokenData] ────────────────────────────────────────── + // An account created by `add_pinata_token_program` must have non-default + // `data` (specifically `vec![3; 33]` encoded as `Data`). + // Catches state.rs:370 (delete `data` from the struct literal). + let pt_id = AccountId::new([0xABu8; 32]); + let mut pinata_state = V03State::new_with_genesis_accounts(&[], vec![], 0); + pinata_state.add_pinata_token_program(pt_id); + let pt = pinata_state.get_account_by_id(pt_id); + + assert_ne!( + pt.program_owner, + default_account.program_owner, + "INVARIANT VIOLATION [PinataTokenProgramOwner]: \ + add_pinata_token_program must set a non-default program_owner on the account", + ); + assert_ne!( + pt.data, + default_account.data, + "INVARIANT VIOLATION [PinataTokenData]: \ + add_pinata_token_program must set non-default data on the account", + ); +}); diff --git a/scripts/mutants-corpus-test.sh b/scripts/mutants-corpus-test.sh index 7ad68301..0b409f17 100755 --- a/scripts/mutants-corpus-test.sh +++ b/scripts/mutants-corpus-test.sh @@ -37,6 +37,7 @@ targets=( fuzz_multi_block_state_sequence fuzz_sequencer_vs_replayer fuzz_merkle_tree + fuzz_genesis_invariants ) # cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.). From 9bdb070bbfb625d818423e3e1eab463337c45b0d Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 9 Jun 2026 14:20:10 +0800 Subject: [PATCH 19/31] feat: add parallel fuzzing recipe --- Justfile | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Justfile b/Justfile index 947f9896..50cab0f8 100644 --- a/Justfile +++ b/Justfile @@ -39,6 +39,51 @@ fuzz TIME="30": cargo fuzz run "$target" "corpus/libfuzz/$target" -- -max_total_time={{TIME}} done +# Run ALL fuzz targets with a bounded job pool for TIME seconds each (default: 30). +# At most JOBS targets run simultaneously (default: 4); as soon as one finishes +# the next queued target is launched, so the pool stays full until all targets +# have been processed. +# Targets are discovered automatically from fuzz/Cargo.toml — no edit needed +# here when a new [[bin]] entry is added. +# +# Usage: just fuzz-parallel # all targets, 30 s each, 4 in parallel +# just fuzz-parallel 120 # all targets, 120 s each, 4 in parallel +# just fuzz-parallel 120 8 # all targets, 120 s each, 8 in parallel +fuzz-parallel TIME="30" JOBS="4": + #!/bin/bash + set -euo pipefail + TARGETS=($(cargo fuzz list 2>/dev/null)) + total=${#TARGETS[@]} + echo "Targets: $total | max parallel: {{JOBS}} | time per target: {{TIME}}s" + echo "" + PIDS=() + failed=0 + + for target in "${TARGETS[@]}"; do + # Wait for a free slot (block until running jobs < JOBS) + while [ "$(jobs -rp | wc -l | tr -d ' ')" -ge "{{JOBS}}" ]; do + # bash 4.3+: wait -n returns when any single child exits + wait -n 2>/dev/null || sleep 0.5 + done + echo "=== launching $target ({{TIME}}s) ===" + mkdir -p "corpus/libfuzz/$target" + cargo fuzz run "$target" "corpus/libfuzz/$target" -- -max_total_time={{TIME}} & + PIDS+=($!) + done + + # Drain the remaining running targets + for pid in "${PIDS[@]}"; do + wait "$pid" || { echo "WARNING: PID $pid exited with non-zero status"; failed=$((failed + 1)); } + done + + echo "" + if [ "$failed" -eq 0 ]; then + echo "✓ All $total target(s) finished successfully." + else + echo "WARNING: $failed of $total target(s) reported errors." + exit 1 + fi + # Re-run the saved corpus for one or ALL targets (regression mode, no new mutations). # When TARGET is omitted every registered target is replayed in sequence. # Usage: just fuzz-regression # all targets From c9d37b88d1382f452d28a0b2d363d370d5bb81f7 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 9 Jun 2026 23:19:53 +0800 Subject: [PATCH 20/31] fix: exclude mutant backup dir --- .gitignore | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 62affb83..0459c4b6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,9 +31,8 @@ fuzz/coverage/ # Local mutation-testing reports (caught.txt, missed.txt, etc.) # Created by `just mutants-harness` and `just mutants-protocol`. mutants.out/ -mutants-harness.out/ -mutants-protocol.out/ -mutants-protocol.out.backup/ +mutants-harness.out* +mutants-protocol.out* # ── Misc ────────────────────────────────────────────────────────────────────── # Performance baseline output from `just perf-baseline` or CI From 9cb0d43c40a1be93f177f3c2854bccc987062df1 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 10 Jun 2026 16:29:05 +0800 Subject: [PATCH 21/31] chore: add new fuzz targets to cover 40 missed mutants --- fuzz/Cargo.lock | 1 - fuzz/Cargo.toml | 37 ++- fuzz/fuzz_targets/fuzz_common_invariants.rs | 171 +++++++++++ .../fuzz_encoding_privacy_preserving.rs | 203 +++++++++++++ fuzz/fuzz_targets/fuzz_merkle_tree.rs | 212 +++++++------- .../fuzz_nullifier_set_roundtrip.rs | 75 +++++ .../fuzz_privacy_preserving_witness.rs | 236 +++++++++++++++ .../fuzz_system_account_protection.rs | 165 +++++++++++ .../fuzz_transaction_properties.rs | 271 ++++++++++++++++++ scripts/mutants-corpus-test.sh | 6 + 10 files changed, 1272 insertions(+), 105 deletions(-) create mode 100644 fuzz/fuzz_targets/fuzz_common_invariants.rs create mode 100644 fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs create mode 100644 fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs create mode 100644 fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs create mode 100644 fuzz/fuzz_targets/fuzz_system_account_protection.rs create mode 100644 fuzz/fuzz_targets/fuzz_transaction_properties.rs diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 3d8a3101..413470e0 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -2084,7 +2084,6 @@ dependencies = [ "lee", "lee_core", "libfuzzer-sys", - "sha2", "testnet_initial_state", ] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 762b496e..4b615487 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -44,7 +44,6 @@ libfuzzer-sys = { version = "0.4", optional = true } afl = { version = "0.15", optional = true } arbitrary = { version = "1", features = ["derive"] } borsh = "1" -sha2 = "0.10" nssa = { path = "../../logos-execution-zone/lee/state_machine", package = "lee" } nssa_core = { path = "../../logos-execution-zone/lee/state_machine/core", package = "lee_core" } common = { path = "../../logos-execution-zone/lez/common" } @@ -132,3 +131,39 @@ name = "fuzz_genesis_invariants" path = "fuzz_targets/fuzz_genesis_invariants.rs" test = false bench = false + +[[bin]] +name = "fuzz_common_invariants" +path = "fuzz_targets/fuzz_common_invariants.rs" +test = false +bench = false + +[[bin]] +name = "fuzz_transaction_properties" +path = "fuzz_targets/fuzz_transaction_properties.rs" +test = false +bench = false + +[[bin]] +name = "fuzz_privacy_preserving_witness" +path = "fuzz_targets/fuzz_privacy_preserving_witness.rs" +test = false +bench = false + +[[bin]] +name = "fuzz_encoding_privacy_preserving" +path = "fuzz_targets/fuzz_encoding_privacy_preserving.rs" +test = false +bench = false + +[[bin]] +name = "fuzz_nullifier_set_roundtrip" +path = "fuzz_targets/fuzz_nullifier_set_roundtrip.rs" +test = false +bench = false + +[[bin]] +name = "fuzz_system_account_protection" +path = "fuzz_targets/fuzz_system_account_protection.rs" +test = false +bench = false diff --git a/fuzz/fuzz_targets/fuzz_common_invariants.rs b/fuzz/fuzz_targets/fuzz_common_invariants.rs new file mode 100644 index 00000000..d4337ba2 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_common_invariants.rs @@ -0,0 +1,171 @@ +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +//! Fuzz target: common-crate and low-level type invariants. +//! +//! This target is **input-independent**: the fuzz input is always ignored. +//! It asserts deterministic invariants about types in `lez/common` and +//! low-level `lee` types that are not exercised by higher-level state-transition +//! targets. +//! +//! # Corpus note +//! +//! A single `\x00` seed file is sufficient — the input bytes are never read. + +use common::{HashType, config::BasicAuth}; +use nssa::{ + privacy_preserving_transaction::circuit::Proof, + program::Program, + program_deployment_transaction::Message as DeployMessage, + program_methods::{ + AUTHENTICATED_TRANSFER_ELF, TOKEN_ELF, + }, +}; + +fuzz_props::fuzz_entry!(|_data: &[u8]| { + // ── INVARIANT [HashTypeAsRefLength] ──────────────────────────────────────── + // `HashType::as_ref()` must always return exactly 32 bytes. + // Catches mutations that return an empty slice or a slice of the wrong size. + let all_ones = HashType([1_u8; 32]); + assert_eq!( + all_ones.as_ref().len(), + 32, + "INVARIANT VIOLATION [HashTypeAsRefLength]: HashType::as_ref must return 32 bytes", + ); + + let zero = HashType::default(); + assert_eq!( + zero.as_ref().len(), + 32, + "INVARIANT VIOLATION [HashTypeAsRefLength]: HashType::as_ref on default must return 32 bytes", + ); + + // ── INVARIANT [HashTypeAsRefBytes] ──────────────────────────────────────── + // `HashType::as_ref()` must return the exact inner bytes. + // Catches mutations that return `vec![0]` or `vec![1]` instead of `&self.0`. + let known = [0x42_u8; 32]; + let hash = HashType(known); + assert_eq!( + hash.as_ref(), + &known, + "INVARIANT VIOLATION [HashTypeAsRefBytes]: HashType::as_ref must return the inner [u8;32]", + ); + + // ── INVARIANT [BasicAuthPasswordPreserved] ─────────────────────────────── + // Parsing "user:password" must preserve the non-empty password as `Some`. + // Catches the mutation that deletes `!` in the `.filter(|p| !p.is_empty())` + // predicate, which would flip the logic and accept only empty passwords. + let auth: BasicAuth = "user:secret" + .parse() + .expect("INVARIANT VIOLATION: 'user:secret' must parse as BasicAuth"); + assert_eq!( + auth.password.as_deref(), + Some("secret"), + "INVARIANT VIOLATION [BasicAuthPasswordPreserved]: \ + parsing 'user:secret' must give password = Some(\"secret\")", + ); + + let auth2: BasicAuth = "alice:hunter2" + .parse() + .expect("INVARIANT VIOLATION: 'alice:hunter2' must parse"); + assert_eq!( + auth2.password.as_deref(), + Some("hunter2"), + "INVARIANT VIOLATION [BasicAuthPasswordPreserved]: \ + password must match the part after the colon", + ); + + // ── INVARIANT [BasicAuthEmptyPasswordIsNone] ───────────────────────────── + // Parsing "user:" (empty password) must give `password = None`. + // With the `!` deleted, this would become `Some("")` instead of `None`. + let auth_empty: BasicAuth = "user:" + .parse() + .expect("INVARIANT VIOLATION: 'user:' must parse as BasicAuth"); + assert_eq!( + auth_empty.password, + None, + "INVARIANT VIOLATION [BasicAuthEmptyPasswordIsNone]: \ + an empty password (trailing colon) must give password = None", + ); + + // ── INVARIANT [ProgramElfNonEmpty] ─────────────────────────────────────── + // `Program::elf()` must return a non-empty byte slice. + // Catches the mutation that returns `Vec::leak(Vec::new())`. + let at_prog = Program::authenticated_transfer_program(); + assert!( + !at_prog.elf().is_empty(), + "INVARIANT VIOLATION [ProgramElfNonEmpty]: \ + Program::authenticated_transfer_program().elf() must not be empty", + ); + + let token_prog = Program::token(); + assert!( + !token_prog.elf().is_empty(), + "INVARIANT VIOLATION [ProgramElfNonEmpty]: \ + Program::token().elf() must not be empty", + ); + + // ── INVARIANT [ProgramElfCorrect] ──────────────────────────────────────── + // `Program::elf()` must return exactly the compile-time bytecode constant. + // Catches the mutations that return `vec![0]` or `vec![1]`. + assert_eq!( + at_prog.elf(), + AUTHENTICATED_TRANSFER_ELF, + "INVARIANT VIOLATION [ProgramElfCorrect]: \ + Program::authenticated_transfer_program().elf() must equal AUTHENTICATED_TRANSFER_ELF", + ); + + assert_eq!( + token_prog.elf(), + TOKEN_ELF, + "INVARIANT VIOLATION [ProgramElfCorrect]: \ + Program::token().elf() must equal TOKEN_ELF", + ); + + // ── INVARIANT [ProofIntoInnerRoundtrip] ────────────────────────────────── + // `Proof::from_inner(bytes).into_inner()` must return the original bytes. + // Catches the mutations that return `vec![]`, `vec![0]`, or `vec![1]`. + let proof_bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF]; + let proof = Proof::from_inner(proof_bytes.clone()); + assert_eq!( + proof.into_inner(), + proof_bytes, + "INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \ + Proof::from_inner(b).into_inner() must return b", + ); + + // Also test with an empty proof (round-trip must preserve emptiness). + let empty_proof = Proof::from_inner(vec![]); + assert!( + empty_proof.into_inner().is_empty(), + "INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \ + empty Proof::from_inner(vec![]).into_inner() must be empty", + ); + + // And with a single non-zero byte: + let single = Proof::from_inner(vec![0xFF]); + assert_eq!( + single.into_inner(), + vec![0xFF_u8], + "INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \ + Proof from single byte must round-trip correctly", + ); + + // ── INVARIANT [DeployMessageBytecodeRoundtrip] ──────────────────────────── + // `Message::new(bytecode).into_bytecode()` must return the original bytecode. + // Catches the mutations that return `vec![]`, `vec![0]`, or `vec![1]`. + let bytecode = vec![0x7F_u8, 0x45, 0x4C, 0x46]; // ELF magic + let msg = DeployMessage::new(bytecode.clone()); + assert_eq!( + msg.into_bytecode(), + bytecode, + "INVARIANT VIOLATION [DeployMessageBytecodeRoundtrip]: \ + Message::new(b).into_bytecode() must return b", + ); + + // Empty bytecode round-trip: + let empty_msg = DeployMessage::new(vec![]); + assert!( + empty_msg.into_bytecode().is_empty(), + "INVARIANT VIOLATION [DeployMessageBytecodeRoundtrip]: \ + empty bytecode must round-trip as empty", + ); +}); diff --git a/fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs b/fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs new file mode 100644 index 00000000..26defc5c --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs @@ -0,0 +1,203 @@ +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +//! Fuzz target: privacy-preserving encoding invariants. +//! +//! Tests that `to_bytes` / `from_bytes` round-trips work correctly for the +//! privacy-preserving `Message` type, and that `try_from_circuit_output` +//! validates ciphertext-to-key length matching. +//! +//! `PrivacyPreservingTransaction` is also tested for serialisation stability +//! (non-empty, deterministic bytes) without requiring a real ZK receipt. + +use nssa::{ + AccountId, PrivateKey, PublicKey, + PrivacyPreservingTransaction, + privacy_preserving_transaction::{ + Message as PPMessage, + WitnessSet as PPWitnessSet, + circuit::Proof, + }, +}; +use nssa_core::{ + PrivacyPreservingCircuitOutput, + account::Nonce, + program::{BlockValidityWindow, TimestampValidityWindow}, +}; + +/// Build a minimal `Message` with no private state. +fn minimal_message() -> PPMessage { + let addr = AccountId::from( + &PublicKey::new_from_private_key( + &PrivateKey::try_new([1_u8; 32]).expect("known-good"), + ), + ); + PPMessage { + public_account_ids: vec![addr], + nonces: vec![Nonce::from(0_u128)], + public_post_states: vec![], + encrypted_private_post_states: vec![], + new_commitments: vec![], + new_nullifiers: vec![], + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), + } +} + +fuzz_props::fuzz_entry!(|data: &[u8]| { + // ── INVARIANT [MessageEncodingRoundtrip] ────────────────────────────────── + // `Message::to_bytes()` followed by `Message::from_bytes()` must reproduce + // the original message. Catches mutations that return `vec![]`, `vec![0]`, + // or `vec![1]` — these break round-trip identity. + { + let msg = minimal_message(); + let encoded = msg.to_bytes(); + + // Non-empty: catches `→ vec![]` + assert!( + !encoded.is_empty(), + "INVARIANT VIOLATION [MessageEncodingRoundtrip]: \ + Message::to_bytes must not return an empty vec", + ); + + let decoded = PPMessage::from_bytes(&encoded) + .expect("INVARIANT VIOLATION [MessageEncodingRoundtrip]: \ + from_bytes(to_bytes(msg)) must succeed"); + + let re_encoded = decoded.to_bytes(); + assert_eq!( + encoded, + re_encoded, + "INVARIANT VIOLATION [MessageEncodingRoundtrip]: \ + encode(decode(encode(msg))) != encode(msg)", + ); + } + + // ── INVARIANT [TxEncodingNonEmpty] / [TxEncodingDeterministic] ──────────── + // `PrivacyPreservingTransaction::to_bytes()` must return a non-empty byte + // slice and be deterministic. Catches mutations that return `vec![]` etc. + { + let key = PrivateKey::try_new([1_u8; 32]).expect("known-good"); + let msg = minimal_message(); + let proof = Proof::from_inner(vec![0xDE_u8, 0xAD, 0xBE, 0xEF]); + let ws = PPWitnessSet::for_message(&msg, proof, &[&key]); + let tx = PrivacyPreservingTransaction::new(msg, ws); + + let bytes1 = tx.to_bytes(); + assert!( + !bytes1.is_empty(), + "INVARIANT VIOLATION [TxEncodingNonEmpty]: \ + PrivacyPreservingTransaction::to_bytes must not be empty", + ); + + let bytes2 = tx.to_bytes(); + assert_eq!( + bytes1, + bytes2, + "INVARIANT VIOLATION [TxEncodingDeterministic]: \ + to_bytes must be deterministic — called twice, got different results", + ); + + // Verify round-trip for the full transaction: + let decoded = PrivacyPreservingTransaction::from_bytes(&bytes1) + .expect("INVARIANT VIOLATION: round-trip decode must succeed"); + assert_eq!( + bytes1, + decoded.to_bytes(), + "INVARIANT VIOLATION [TxEncodingDeterministic]: \ + encode(decode(encode(tx))) != encode(tx)", + ); + } + + // ── INVARIANT [LengthMatchAccepted] ─────────────────────────────────────── + // When public_keys.len() == ciphertexts.len() == 0, `try_from_circuit_output` + // must succeed. + // + // Original check: `if public_keys.len() != output.ciphertexts.len() { Err }` + // With mutation `!=` → `==`: `if 0 == 0` → `true` → Err is returned. + // Our assertion that the call SUCCEEDS catches the mutation. + { + let empty_output = PrivacyPreservingCircuitOutput { + public_pre_states: vec![], + public_post_states: vec![], + new_commitments: vec![], + new_nullifiers: vec![], + ciphertexts: vec![], + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), + }; + + let result = PPMessage::try_from_circuit_output( + vec![], // public_account_ids + vec![], // nonces + vec![], // public_keys (0 entries) + empty_output, + ); + assert!( + result.is_ok(), + "INVARIANT VIOLATION [LengthMatchAccepted]: \ + try_from_circuit_output must accept when keys(0) == ciphertexts(0), \ + got: {:?} — \ + possible mutation: != changed to == in the length check", + result.err(), + ); + } + + // ── Raw fuzz decode tests ───────────────────────────────────────────────── + // Fuzz the Message decoder for no-panic and canonical round-trip. + { + // No-panic on arbitrary bytes: + let _ = PPMessage::from_bytes(data); + + // Canonical round-trip: if fuzz bytes decode, re-encoding must reproduce them. + if let Ok(msg) = PPMessage::from_bytes(data) { + let re_encoded = msg.to_bytes(); + assert_eq!( + data, + re_encoded.as_slice(), + "INVARIANT VIOLATION: PP Message decoded from raw bytes but \ + re-encoding differs (non-canonical encoding accepted)", + ); + } + } + + // ── Varied-size message round-trips ────────────────────────────────────── + // Verify round-trip for several multi-account messages. + for n_accounts in [0, 1, 2, 3] { + let mut account_ids = Vec::new(); + let mut nonces = Vec::new(); + for i in 0..n_accounts { + let key_bytes = [i + 1_u8; 32]; + if let Ok(key) = PrivateKey::try_new(key_bytes) { + let pk = PublicKey::new_from_private_key(&key); + account_ids.push(AccountId::from(&pk)); + nonces.push(Nonce::from(i as u128)); + } + } + + let msg = PPMessage { + public_account_ids: account_ids, + nonces, + public_post_states: vec![], + encrypted_private_post_states: vec![], + new_commitments: vec![], + new_nullifiers: vec![], + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), + }; + + let encoded = msg.to_bytes(); + assert!( + !encoded.is_empty(), + "INVARIANT VIOLATION [MessageEncodingRoundtrip]: \ + Message::to_bytes must not be empty for a {n_accounts}-account message", + ); + + let decoded = PPMessage::from_bytes(&encoded) + .expect("round-trip must succeed for well-formed message"); + assert_eq!( + encoded, + decoded.to_bytes(), + "INVARIANT VIOLATION [MessageEncodingRoundtrip]: \ + round-trip failed for {n_accounts}-account message", + ); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_merkle_tree.rs b/fuzz/fuzz_targets/fuzz_merkle_tree.rs index a88249b6..3e54be66 100644 --- a/fuzz/fuzz_targets/fuzz_merkle_tree.rs +++ b/fuzz/fuzz_targets/fuzz_merkle_tree.rs @@ -1,130 +1,136 @@ #![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] -//! Fuzz target: `MerkleTree` structural invariants +//! Fuzz target: Merkle-tree structural invariants, exercised through the +//! **public** commitment-set API (no `pub mod merkle_tree` patch required). //! -//! Covered code paths (all in `lee/state_machine/src/merkle_tree/mod.rs`): +//! The commitment set in `V03State` is a thin wrapper around the internal +//! `MerkleTree`: //! //! ```text -//! MerkleTree::with_capacity(1) ← initial capacity forces reallocate_to_double_capacity -//! MerkleTree::insert(value) ← per-value; also triggers reallocate_to_double_capacity -//! MerkleTree::root() ← sampled once after all inserts -//! MerkleTree::get_authentication_path_for(index) ← per-value -//! prev_power_of_two ← exercised inside reallocate_to_double_capacity +//! V03State::commitment_set_digest() → MerkleTree::root() (→ root_index) +//! V03State::get_proof_for_commitment(c) → (index, MerkleTree::get_authentication_path_for(index)) +//! CommitmentSet::extend(commitments) → MerkleTree::insert(value) per commitment +//! compute_digest_for_path(c, proof) → canonical leaf→root recomputation //! ``` //! +//! Inserting commitments via `V03State::new_with_genesis_accounts` therefore +//! drives `insert`, `root`/`root_index`, `get_authentication_path_for`, `depth`, +//! `get_node`/`set_node`, and — once the count exceeds the genesis capacity (32) +//! — `reallocate_to_double_capacity` and `prev_power_of_two`. +//! +//! Because the genesis commitment set has a fixed capacity of 32, a *small* +//! number of commitments exercises the partial-fill regime (`depth < +//! capacity_depth`, i.e. `root_index`'s else-branch), while a *large* number +//! (> 31) forces one or more reallocations. A single target therefore covers +//! both regimes — the committed corpus carries a small partial-fill seed +//! (`seed_partial6`) and a large reallocation seed (`seed_realloc40`). +//! //! # Input format //! -//! The raw fuzz bytes are sliced into 32-byte chunks; each chunk becomes one -//! value inserted into the tree. This makes the format trivial to reason about -//! and lets us seed the corpus with well-known test vectors. +//! Each 32-byte chunk of the fuzz input is reinterpreted as an `AccountId`, from +//! which a distinct `Commitment` is derived (`Commitment::new`). Duplicate +//! chunks are dropped so every inserted commitment is unique and lands at a +//! distinct, sequential tree index. The number of distinct chunks selects the +//! fill regime (partial-fill vs. reallocation). //! -//! # Invariants checked +//! # Invariants //! -//! 1. **InsertionIndex** — `insert(value)` returns the sequential 0-based index. -//! 2. **AuthPathSome** — `get_authentication_path_for(i)` is `Some` for every -//! `i < length`. -//! 3. **AuthPathValid** — every returned path re-hashes (SHA-256, same hash -//! functions used by the production code) to the value reported by `root()`. -//! 4. **OutOfBoundsNone** — `get_authentication_path_for(length)` returns `None`. +//! 1. **ProofSome** — every inserted commitment has a membership proof. +//! 2. **ProofValid** — `compute_digest_for_path(commitment, proof)` reproduces +//! `commitment_set_digest()` for every inserted commitment. This is the core +//! check: it independently recomputes the root from the leaf + authentication +//! path and compares against the tree's reported root, catching arithmetic +//! bugs in `root_index`, `insert`, and the path-walk. +//! 3. **IndicesSequential** — the genesis dummy commitment occupies index 0, so +//! `N` distinct user commitments must occupy exactly indices `1..=N`. Catches +//! `insert -> 0` / `insert -> 1` return-value mutations. +//! 4. **NonMembershipNone** — a commitment that was never inserted has no proof. -use sha2::{Digest as _, Sha256}; +use std::collections::HashSet; -// ─── Reference hash helpers (mirrors the private functions in merkle_tree/mod.rs) ─── - -/// SHA-256 of a single 32-byte leaf value. Mirrors `hash_value`. -fn sha256_one(v: &[u8; 32]) -> [u8; 32] { - let mut h = Sha256::new(); - h.update(v); - h.finalize().into() -} - -/// SHA-256 of two concatenated 32-byte nodes. Mirrors `hash_two`. -fn sha256_two(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { - let mut h = Sha256::new(); - h.update(left); - h.update(right); - h.finalize().into() -} - -/// Reference implementation of authentication-path verification. -/// -/// Mirrors `verify_authentication_path` from the test module inside -/// `lee/state_machine/src/merkle_tree/mod.rs`. -/// -/// Algorithm: -/// result ← SHA-256(value) -/// for each sibling in path: -/// if level_index is even → result is the LEFT child → hash(result, sibling) -/// if level_index is odd → result is the RIGHT child → hash(sibling, result) -/// level_index >>= 1 -/// return result == root -fn verify_auth_path(value: &[u8; 32], index: usize, path: &[[u8; 32]], root: &[u8; 32]) -> bool { - let mut result = sha256_one(value); - let mut level_index = index; - for sibling in path { - let is_left_child = level_index & 1 == 0; - result = if is_left_child { - sha256_two(&result, sibling) - } else { - sha256_two(sibling, &result) - }; - level_index >>= 1; - } - &result == root -} +use nssa::V03State; +use nssa_core::{ + Commitment, Nullifier, + account::{Account, AccountId}, + compute_digest_for_path, +}; fuzz_props::fuzz_entry!(|data: &[u8]| { - // Treat each 32-byte chunk as one leaf value. Discard any trailing - // incomplete chunk. - let values: Vec<[u8; 32]> = data - .chunks_exact(32) - .map(|c| c.try_into().expect("chunks_exact(32) always yields [u8;32]")) - .collect(); + // Reinterpret each 32-byte chunk as an AccountId; derive one commitment each. + // Dedup chunks so commitments are distinct and indices are clean. + let mut seen: HashSet<[u8; 32]> = HashSet::new(); + let mut pairs: Vec<(Commitment, Nullifier)> = Vec::new(); + for chunk in data.chunks_exact(32) { + let bytes: [u8; 32] = chunk.try_into().expect("chunks_exact(32) yields [u8;32]"); + if !seen.insert(bytes) { + continue; // skip duplicate account ids + } + let commitment = Commitment::new(&AccountId::new(bytes), &Account::default()); + // A distinct nullifier per pair (content is irrelevant to the merkle tree). + let nullifier = Nullifier::from_byte_array(bytes); + pairs.push((commitment, nullifier)); + } - // Nothing to test with an empty input. - if values.is_empty() { + if pairs.is_empty() { return; } - // Start with capacity=1 so the very first pair of insertions triggers - // `reallocate_to_double_capacity`, and each subsequent power-of-two boundary - // triggers it again. This exercises `prev_power_of_two`, the copy loop, - // and the capacity / length bookkeeping inside the reallocation path. - let mut tree = nssa::merkle_tree::MerkleTree::with_capacity(1); + // Keep the commitments so we can query their proofs after the state moves `pairs`. + let commitments: Vec = pairs.iter().map(|(c, _)| c.clone()).collect(); - // ── INVARIANT [InsertionIndex] ──────────────────────────────────────────── - // insert() must return 0, 1, 2, … in order. - for (expected_index, &value) in values.iter().enumerate() { - let actual_index = tree.insert(value); + // Genesis inserts DUMMY_COMMITMENT at index 0, then our commitments at 1..=N. + let state = V03State::new_with_genesis_accounts(&[], pairs, 0); + let digest = state.commitment_set_digest(); + + let mut indices: Vec = Vec::with_capacity(commitments.len()); + for commitment in &commitments { + // ── INVARIANT [ProofSome] ───────────────────────────────────────────── + let proof = state.get_proof_for_commitment(commitment).expect( + "INVARIANT VIOLATION [ProofSome]: \ + get_proof_for_commitment returned None for an inserted commitment", + ); + + // ── INVARIANT [ProofValid] ──────────────────────────────────────────── + // Recompute the root from the leaf + authentication path and compare to + // the tree's reported digest. A bug in root_index / insert / the path + // walk makes these disagree. assert_eq!( - actual_index, - expected_index, - "INVARIANT VIOLATION [InsertionIndex]: \ - insert returned {actual_index} but expected {expected_index}", + compute_digest_for_path(commitment, &proof), + digest, + "INVARIANT VIOLATION [ProofValid]: \ + membership proof for a commitment at index {} does not recompute to \ + commitment_set_digest()", + proof.0, + ); + + indices.push(proof.0); + } + + // ── INVARIANT [IndicesSequential] ───────────────────────────────────────── + // The dummy commitment holds index 0; our N distinct commitments must hold + // exactly indices 1..=N. + indices.sort_unstable(); + for (k, &idx) in indices.iter().enumerate() { + assert_eq!( + idx, + k + 1, + "INVARIANT VIOLATION [IndicesSequential]: \ + inserted commitments must occupy sequential indices 1..=N (dummy at 0); \ + got index {idx} at sorted position {k}", ); } - let root = tree.root(); - - // ── INVARIANTS [AuthPathSome] and [AuthPathValid] ───────────────────────── - for (index, value) in values.iter().enumerate() { - let path = tree - .get_authentication_path_for(index) - .expect("INVARIANT VIOLATION [AuthPathSome]: \ - get_authentication_path_for returned None for a valid index"); - + // ── INVARIANT [NonMembershipNone] ───────────────────────────────────────── + // A commitment derived from an account id that was NOT inserted must have no + // proof. Use an all-0xFF sentinel id and only assert when it is genuinely + // absent from the inserted set. + let sentinel_bytes = [0xFF_u8; 32]; + if !seen.contains(&sentinel_bytes) { + let absent = + Commitment::new(&AccountId::new(sentinel_bytes), &Account::default()); assert!( - verify_auth_path(value, index, &path, &root), - "INVARIANT VIOLATION [AuthPathValid]: \ - authentication path for index {index} does not re-hash to root()", + state.get_proof_for_commitment(&absent).is_none(), + "INVARIANT VIOLATION [NonMembershipNone]: \ + get_proof_for_commitment returned Some for a commitment never inserted", ); } - - // ── INVARIANT [OutOfBoundsNone] ─────────────────────────────────────────── - // The index one past the last inserted element must yield None. - assert!( - tree.get_authentication_path_for(values.len()).is_none(), - "INVARIANT VIOLATION [OutOfBoundsNone]: \ - get_authentication_path_for({}) should return None but returned Some", - values.len(), - ); }); diff --git a/fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs b/fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs new file mode 100644 index 00000000..4a7338df --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs @@ -0,0 +1,75 @@ +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +//! Fuzz target: `NullifierSet` Borsh serialisation. +//! +//! The `NullifierSet` has a hand-written `BorshDeserialize` (in +//! `lee/state_machine/src/state.rs`) that rejects duplicate nullifiers via +//! `if !set.insert(n)`. This target verifies that: +//! +//! 1. States containing distinct nullifiers survive a Borsh round-trip. The +//! `delete-!` mutation at `state.rs:104` flips the dedup check so that +//! `deserialize_reader` errors on the *first* (non-duplicate) element; a state +//! with two distinct nullifiers then fails to deserialise, tripping Part 1. +//! 2. Feeding arbitrary fuzz bytes to the `V03State` deserialiser never panics. +//! +//! # Corpus note +//! +//! A single `\x00` seed is sufficient — Part 1 uses fixed inputs and catches the +//! `delete-!` mutation without fuzz-driven state. + +use nssa::{Account, AccountId, V03State, system_faucet_account_id}; +use nssa_core::{Commitment, Nullifier}; + +fuzz_props::fuzz_entry!(|data: &[u8]| { + // ── Part 1: State with nullifiers — Borsh round-trip ───────────────────── + // Create a V03State that contains committed nullifiers via the + // `initial_private_accounts` constructor argument. + // + // With state.rs:105 mutation (delete `!`): + // - `BorshDeserialize for NullifierSet` returns `Err` on the FIRST element + // - `borsh::from_slice::(&bytes)` returns Err + // - The assert_eq below fires → mutation CAUGHT + { + // Two deterministic nullifier values (use from_byte_array): + let null1 = Nullifier::from_byte_array([0xAA_u8; 32]); + let null2 = Nullifier::from_byte_array([0xBB_u8; 32]); + // Commitment::new takes (&AccountId, &Account): + let comm1 = Commitment::new(&AccountId::new([0x11_u8; 32]), &Account::default()); + let comm2 = Commitment::new(&AccountId::new([0x22_u8; 32]), &Account::default()); + + // Build a state that holds two nullifiers in its private state. + let state = V03State::new_with_genesis_accounts( + &[(system_faucet_account_id(), 0)], + vec![(comm1, null1), (comm2, null2)], + 0, + ); + + // Serialise the state: + let bytes = borsh::to_vec(&state) + .expect("BorshSerialize for V03State must not fail"); + assert!(!bytes.is_empty()); + + // Deserialise: with the mutation, this returns Err for any state with + // nullifiers, triggering the assertion below. + let state2 = borsh::from_slice::(&bytes) + .expect("INVARIANT VIOLATION [NullifierSetRoundtrip]: \ + borsh::from_slice of a state with nullifiers must succeed \ + (mutation delete-! in NullifierSet::deserialize_reader detected)"); + + // Re-encode and verify idempotence: + let bytes2 = borsh::to_vec(&state2) + .expect("second BorshSerialize must not fail"); + assert_eq!( + bytes, + bytes2, + "INVARIANT VIOLATION [NullifierSetRoundtrip]: \ + encode(decode(encode(state))) != encode(state) — \ + NullifierSet round-trip is not idempotent", + ); + } + + // ── Part 2: Fuzz-driven raw bytes ───────────────────────────────────────── + // Feed raw fuzz bytes through V03State deserialiser — no panic allowed. + { + let _ = borsh::from_slice::(data); // NoPanic: Ok or Err, no panic + } +}); diff --git a/fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs b/fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs new file mode 100644 index 00000000..a8b8c1d9 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs @@ -0,0 +1,236 @@ +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +//! Fuzz target: `privacy_preserving_transaction::WitnessSet` invariants. +//! +//! Mirrors `fuzz_witness_set_verification` but for the privacy-preserving +//! witness set, which additionally holds a ZK `Proof` alongside the ECDSA +//! signatures. +//! +//! # Invariants +//! +//! 1. **CorrectVerification** — a `WitnessSet` built for message A via +//! `WitnessSet::for_message` must pass `signatures_are_valid_for(A)`. +//! +//! 2. **MessageIsolation** — the same `WitnessSet` must NOT pass +//! `signatures_are_valid_for(B)` when B borsh-encodes differently from A. +//! +//! 3. **SignaturesAndPublicKeysNonEmpty** — after `for_message` with N keys, +//! `signatures_and_public_keys()` must return N entries. +//! +//! 4. **SignerIdsMatchWitnessKeys** — `PrivacyPreservingTransaction::signer_account_ids` +//! must equal `AccountId::from(pk)` for every key in the witness set. + +use arbitrary::{Arbitrary, Unstructured}; +use fuzz_props::arbitrary_types::ArbPrivateKey; +use nssa::{ + AccountId, PrivateKey, PublicKey, + privacy_preserving_transaction::{ + Message as PPMessage, + WitnessSet as PPWitnessSet, + circuit::Proof, + }, + PrivacyPreservingTransaction, +}; +use nssa_core::{ + account::Nonce, + program::{BlockValidityWindow, TimestampValidityWindow}, +}; + +/// Build a minimal `Message` for testing — no commitments, no nullifiers, +/// no encrypted states. Sufficient to test signature binding. +fn minimal_message(account_ids: Vec, nonces: Vec) -> PPMessage { + PPMessage { + public_account_ids: account_ids, + nonces, + public_post_states: vec![], + encrypted_private_post_states: vec![], + new_commitments: vec![], + new_nullifiers: vec![], + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), + } +} + +/// Build a minimal (fake) `Proof` — bytes don't form a real ZK receipt but +/// are valid for struct construction and serialisation. +fn fake_proof() -> Proof { + Proof::from_inner(vec![0xAB_u8; 32]) +} + +fuzz_props::fuzz_entry!(|data: &[u8]| { + let mut u = Unstructured::new(data); + + // ── Fixed-key deterministic part ────────────────────────────────────────── + // Always runs regardless of input length, ensuring the mutation is caught + // even on an empty corpus. + { + let key1 = PrivateKey::try_new([1_u8; 32]).expect("known-good key"); + let key2 = PrivateKey::try_new([2_u8; 32]).expect("known-good key"); + let pub1 = PublicKey::new_from_private_key(&key1); + let pub2 = PublicKey::new_from_private_key(&key2); + let addr1 = AccountId::from(&pub1); + let addr2 = AccountId::from(&pub2); + + let msg = minimal_message( + vec![addr1, addr2], + vec![Nonce::from(0_u128), Nonce::from(1_u128)], + ); + + let ws = PPWitnessSet::for_message(&msg, fake_proof(), &[&key1, &key2]); + + // ── INVARIANT [SignaturesAndPublicKeysNonEmpty] ─────────────────────── + assert_eq!( + ws.signatures_and_public_keys().len(), + 2, + "INVARIANT VIOLATION [SignaturesAndPublicKeysNonEmpty]: \ + signatures_and_public_keys must return 2 entries for a 2-key witness set", + ); + + // ── INVARIANT [CorrectVerification] ─────────────────────────────────── + assert!( + ws.signatures_are_valid_for(&msg), + "INVARIANT VIOLATION [CorrectVerification]: \ + WitnessSet::for_message produced a witness set that fails \ + signatures_are_valid_for on the same message", + ); + + // ── INVARIANT [SignerIdsMatchWitnessKeys] ───────────────────────────── + // signer_account_ids is pub(crate); derive from signatures_and_public_keys instead. + let signers_from_ws: Vec = ws + .signatures_and_public_keys() + .iter() + .map(|(_, pk)| AccountId::from(pk)) + .collect(); + assert_eq!(signers_from_ws.len(), 2); + assert!(signers_from_ws.contains(&addr1)); + assert!(signers_from_ws.contains(&addr2)); + + // ── INVARIANT [SignerOnlyAccountInAffected] ─────────────────────────── + // `PrivacyPreservingTransaction::affected_public_account_ids` unions + // `signer_account_ids()` with `message.public_account_ids`. To catch the + // `signer_account_ids → vec![]` mutation, build a message whose + // public_account_ids does NOT contain the signer, so the signer can only + // reach `affected` via `signer_account_ids()`. + let isolated_msg = minimal_message( + vec![AccountId::new([0xB1_u8; 32]), AccountId::new([0xB2_u8; 32])], + vec![Nonce::from(0_u128), Nonce::from(1_u128)], + ); + // Sign with key1 — addr1 is (with overwhelming probability) not one of the + // 0xB1/0xB2 placeholder accounts. + if addr1 != AccountId::new([0xB1_u8; 32]) && addr1 != AccountId::new([0xB2_u8; 32]) { + let isolated_ws = PPWitnessSet::for_message(&isolated_msg, fake_proof(), &[&key1]); + let isolated_tx = + PrivacyPreservingTransaction::new(isolated_msg, isolated_ws); + let affected = isolated_tx.affected_public_account_ids(); + assert!( + affected.contains(&addr1), + "INVARIANT VIOLATION [SignerOnlyAccountInAffected]: \ + PP affected_public_account_ids must include the signer {:?} even when it \ + is absent from message.public_account_ids — signer_account_ids() must not \ + return an empty vec", + addr1, + ); + } + + // ── INVARIANT [MessageIsolation] ────────────────────────────────────── + // Build a different message (different nonces) — the witness set for msg + // must NOT validate against msg_b. + let msg_b = minimal_message( + vec![addr1, addr2], + vec![Nonce::from(999_u128), Nonce::from(1000_u128)], + ); + let bytes_a = borsh::to_vec(&msg); + let bytes_b = borsh::to_vec(&msg_b); + if let (Ok(a), Ok(b)) = (bytes_a, bytes_b) { + if a != b { + assert!( + !ws.signatures_are_valid_for(&msg_b), + "INVARIANT VIOLATION [MessageIsolation]: \ + PP WitnessSet for msg accepted for a different msg_b — \ + possible signature-binding bypass", + ); + } + } + + // Single-key variant: + let ws_single = PPWitnessSet::for_message(&msg, fake_proof(), &[&key1]); + assert_eq!(ws_single.signatures_and_public_keys().len(), 1); + + let tx_single = PrivacyPreservingTransaction::new(msg.clone(), ws_single); + // Use affected_public_account_ids (which calls signer_account_ids internally): + let single_affected = tx_single.affected_public_account_ids(); + assert!( + single_affected.contains(&addr1), + "INVARIANT VIOLATION [SignerIdsMatchWitnessKeys]: 1-key tx must include addr1", + ); + } + + // ── Fuzz-driven part ────────────────────────────────────────────────────── + // Generate 0–3 random private keys, build a WitnessSet, verify correct validation. + { + let n_keys = (u8::arbitrary(&mut u).unwrap_or(0) % 4) as usize; + let mut keys = Vec::with_capacity(n_keys); + let mut addrs = Vec::with_capacity(n_keys); + let mut nonces = Vec::with_capacity(n_keys); + + for i in 0..n_keys { + match ArbPrivateKey::arbitrary(&mut u) { + Ok(k) => { + let pk = PublicKey::new_from_private_key(&k.0); + addrs.push(AccountId::from(&pk)); + nonces.push(Nonce::from(i as u128)); + keys.push(k.0); + } + Err(_) => break, + } + } + + if keys.is_empty() { + return; + } + + let msg = minimal_message(addrs.clone(), nonces); + let key_refs: Vec<&PrivateKey> = keys.iter().collect(); + let ws = PPWitnessSet::for_message(&msg, fake_proof(), &key_refs); + + // INVARIANT [SignaturesAndPublicKeysNonEmpty] + assert_eq!( + ws.signatures_and_public_keys().len(), + keys.len(), + "INVARIANT VIOLATION [SignaturesAndPublicKeysNonEmpty]: \ + signatures_and_public_keys count must match number of keys", + ); + + // INVARIANT [CorrectVerification] + assert!( + ws.signatures_are_valid_for(&msg), + "INVARIANT VIOLATION [CorrectVerification]: \ + PP WitnessSet::for_message produced witnesses that fail validation", + ); + + // INVARIANT [SignerIdsMatchWitnessKeys] + // signer_account_ids is pub(crate); verify via affected_public_account_ids + // (which internally calls signer_account_ids) and via signatures_and_public_keys. + let tx = PrivacyPreservingTransaction::new(msg, ws.clone()); + let signer_ids_from_ws: Vec = ws + .signatures_and_public_keys() + .iter() + .map(|(_, pk)| AccountId::from(pk)) + .collect(); + assert_eq!( + signer_ids_from_ws.len(), + addrs.len(), + "INVARIANT VIOLATION [SignerIdsMatchWitnessKeys]: \ + witness set key count must match number of keys provided", + ); + // affected_public_account_ids includes signer IDs: + let affected2 = tx.affected_public_account_ids(); + for addr in &addrs { + assert!( + affected2.contains(addr), + "INVARIANT VIOLATION [SignerIdsMatchWitnessKeys]: \ + affected_public_account_ids must contain {:?}", + addr, + ); + } + } +}); diff --git a/fuzz/fuzz_targets/fuzz_system_account_protection.rs b/fuzz/fuzz_targets/fuzz_system_account_protection.rs new file mode 100644 index 00000000..8555b92b --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_system_account_protection.rs @@ -0,0 +1,165 @@ +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +//! Fuzz target: system-account modification protection. +//! +//! `LeeTransaction::validate_on_state` must reject any transaction that modifies +//! a system account (faucet, bridge, or clock accounts). This is enforced by +//! `validate_doesnt_modify_account` which inspects `ValidatedStateDiff::public_diff()`. +//! +//! # Corpus note +//! +//! This target is **input-independent**. A single `\x00` seed is sufficient. +//! +//! **Performance note**: the `[SystemAccountModificationRejected]` invariant +//! executes a RISC0 program (a native transfer). This is inherently slow +//! (~seconds). Only one corpus file is needed, so the corpus-regression oracle +//! costs one program execution per mutant under test. + +use common::transaction::LeeTransaction; +use nssa::{ + AccountId, PrivateKey, PublicKey, V03State, ValidatedStateDiff, + CLOCK_01_PROGRAM_ACCOUNT_ID, system_bridge_account_id, system_faucet_account_id, +}; + +fuzz_props::fuzz_entry!(|_data: &[u8]| { + // ── INVARIANT [SystemAccountIdsDistinct] ────────────────────────────────── + let faucet_id = system_faucet_account_id(); + let bridge_id = system_bridge_account_id(); + + assert_ne!( + faucet_id, + AccountId::default(), + "INVARIANT VIOLATION [SystemAccountIdsDistinct]: faucet account ID must be non-default", + ); + assert_ne!( + bridge_id, + AccountId::default(), + "INVARIANT VIOLATION [SystemAccountIdsDistinct]: bridge account ID must be non-default", + ); + assert_ne!( + faucet_id, + bridge_id, + "INVARIANT VIOLATION [SystemAccountIdsDistinct]: faucet and bridge must be distinct", + ); + + // ── INVARIANT [ClockInvocationRejected] ────────────────────────────────── + // A native transfer that CREDITS a clock system account modifies exactly one + // system account — the clock account — and that account is *changed* (its + // balance increases from 0). No other system account appears in the diff, so + // this isolates the `validate_doesnt_modify_account` rejection cleanly. + // + // Why not a clock invocation? The clock program writes all three clock + // accounts, but the 01/10/50 clocks tick at different rates, so its diff + // contains BOTH changed and unchanged system accounts. The `!=`→`==` + // mutation then still rejects (citing an unchanged account), so a clock + // invocation cannot distinguish the mutant. Crediting a single clock account + // gives a single, changed system account, which the mutant must accept. + // + // Why not credit the faucet? The faucet holds u128::MAX, so any credit + // overflows and the program execution fails before the protection check is + // reached. A clock account starts at balance 0, so a small credit succeeds. + // + // With mutation `!=` → `==` at transaction.rs:182: + // The clock account is changed (post != pre), so the mutated `post == pre` + // check is false → no error → validate_on_state returns Ok → our assert fires. + // + // With mutation `public_diff → HashMap::new()` at validated_state_diff.rs:479: + // validate_doesnt_modify_account sees an empty map → can never find the + // clock account → returns Ok for every transaction → our assert fires. + { + let sender_key = PrivateKey::try_new([5_u8; 32]).expect("known-good key"); + let sender_pub = PublicKey::new_from_private_key(&sender_key); + let sender_id = AccountId::from(&sender_pub); + + // Fund the sender; clock accounts already exist in genesis (balance 0). + let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000_u128)], vec![], 0); + + // Transfer tokens TO a clock account — credits (changes) that system account. + let tx = common::test_utils::create_transaction_native_token_transfer( + sender_id, + 0, // nonce + CLOCK_01_PROGRAM_ACCOUNT_ID, + 100, // amount credited to the clock account + &sender_key, + ); + + let result = tx.validate_on_state(&state, 1, 0); + + assert!( + result.is_err(), + "INVARIANT VIOLATION [SystemAccountModificationRejected]: \ + validate_on_state must reject a transfer that credits a clock system \ + account. If this fires, either validate_doesnt_modify_account has a logic \ + inversion (!=→==) or public_diff() returns an empty map", + ); + } + + // ── INVARIANT [PublicDiffNonEmptyOnSuccess] ──────────────────────────────── + // For a valid public transaction with signers, the signer accounts must appear + // in public_diff after successful validation (nonces are updated in the diff). + // + // With mutation `public_diff → HashMap::new()`: + // The map is empty → `contains_key(&signer)` returns false → assert fires. + // + // Uses `common::test_utils::create_transaction_native_token_transfer` to + // construct a semantically valid transaction (correct instruction type). + { + let key = PrivateKey::try_new([7_u8; 32]).expect("known-good key"); + let pubkey = PublicKey::new_from_private_key(&key); + let addr = AccountId::from(&pubkey); + + let key2 = PrivateKey::try_new([8_u8; 32]).expect("known-good key"); + let pubkey2 = PublicKey::new_from_private_key(&key2); + let addr2 = AccountId::from(&pubkey2); + + let state = V03State::new_with_genesis_accounts( + &[(addr, 10_000_u128), (addr2, 10_000_u128)], + vec![], + 0, + ); + + // Use the test utility to build a valid native token transfer. + // This uses the correct authenticated_transfer_core::Instruction::Transfer, + // which the program can actually execute without panicking. + let lee_tx = common::test_utils::create_transaction_native_token_transfer( + addr, + 0, // nonce = 0 (matches initial state nonce) + addr2, + 100, // amount + &key, + ); + + if let LeeTransaction::Public(pub_tx) = &lee_tx { + if let Ok(diff) = ValidatedStateDiff::from_public_transaction(&pub_tx, &state, 1, 0) { + let public_diff = diff.public_diff(); + + // The signer/sender (addr) must be in the diff: a native transfer + // debits its balance, so it MUST appear in public_diff. + // If public_diff() returns an empty HashMap, this assert fires. + // + // Note: nonce increments are applied separately during + // `apply_state_diff` via `signer_account_ids` and are NOT recorded + // in `public_diff`, so we do not assert on the nonce field here. + assert!( + public_diff.contains_key(&addr), + "INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \ + public_diff must contain the sender {:?} (its balance is debited) \ + after a successful native transfer \ + (mutation public_diff→HashMap::new() detected)", + addr, + ); + + // The diff must reflect the balance debit on the sender — the + // balance recorded in the diff must differ from the pre-state. + let pre_balance = state.get_account_by_id(addr).balance; + let post_balance = public_diff[&addr].balance; + assert_ne!( + post_balance, + pre_balance, + "INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \ + sender balance in the diff must differ from pre-state after a transfer \ + (pre={pre_balance}, post={post_balance})", + ); + } + } + } +}); diff --git a/fuzz/fuzz_targets/fuzz_transaction_properties.rs b/fuzz/fuzz_targets/fuzz_transaction_properties.rs new file mode 100644 index 00000000..4c11c7bb --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_transaction_properties.rs @@ -0,0 +1,271 @@ +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +//! Fuzz target: transaction property invariants. +//! +//! Tests that key accessor methods on `LeeTransaction`, `PublicTransaction`, and +//! `ValidatedStateDiff` return correct, non-stub values. + +use arbitrary::{Arbitrary, Unstructured}; +use common::transaction::LeeTransaction; +use fuzz_props::arbitrary_types::ArbPrivateKey; +use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state}; +use nssa::{ + AccountId, PrivateKey, PublicKey, ValidatedStateDiff, V03State, + public_transaction::{Message, WitnessSet}, + PublicTransaction, + program::Program, +}; +use nssa_core::account::Nonce; + +fuzz_props::fuzz_entry!(|data: &[u8]| { + let mut u = Unstructured::new(data); + + // ── Part 1: Known-good witness set / transaction using fixed keys ────────── + // Uses deterministic keys so we always have at least one valid transaction. + // This ensures hash, signer_account_ids, into_raw_parts, and affected_accounts + // are always tested, even when the fuzzer input is insufficient for arb generators. + { + let key1 = PrivateKey::try_new([1_u8; 32]).expect("known-good key"); + let key2 = PrivateKey::try_new([2_u8; 32]).expect("known-good key"); + let pub1 = PublicKey::new_from_private_key(&key1); + let pub2 = PublicKey::new_from_private_key(&key2); + let addr1 = AccountId::from(&pub1); + let addr2 = AccountId::from(&pub2); + + let nonces = vec![Nonce::from(0_u128), Nonce::from(0_u128)]; + let message = Message::try_new( + Program::authenticated_transfer_program().id(), + vec![addr1, addr2], + nonces, + 1337_u64, + ) + .expect("known-good message"); + + let ws = WitnessSet::for_message(&message, &[&key1, &key2]); + let pub_tx = PublicTransaction::new(message, ws); + + // ── INVARIANT [SignerIdsNonEmpty] ───────────────────────────────────── + // A transaction signed by 2 keys must expose 2 signer (key, sig) pairs. + // `signer_account_ids` is pub(crate); we verify via the public witness_set API. + let ws_pairs = pub_tx.witness_set().signatures_and_public_keys(); + assert_eq!( + ws_pairs.len(), + 2, + "INVARIANT VIOLATION [SignerIdsNonEmpty]: \ + witness_set signatures_and_public_keys must have 2 entries", + ); + + // ── INVARIANT [IntoRawPartsCount] ───────────────────────────────────── + // `into_raw_parts` must return the same number of pairs as the witness set. + // Catches the mutation that returns `vec![]`. + let ws2 = WitnessSet::for_message(pub_tx.message(), &[&key1, &key2]); + let parts = ws2.into_raw_parts(); + assert_eq!( + parts.len(), + 2, + "INVARIANT VIOLATION [IntoRawPartsCount]: \ + WitnessSet::into_raw_parts must return 2 pairs for a 2-key witness set", + ); + + // ── INVARIANT [AffectedAccountsContainSigners] ─────────────────────── + // `affected_public_account_ids` must include the signer accounts. + // Catches the mutation that returns `vec![]` or `vec![Default::default()]`. + let affected = pub_tx.affected_public_account_ids(); + assert!( + !affected.is_empty(), + "INVARIANT VIOLATION [AffectedAccountsContainSigners]: \ + affected_public_account_ids must be non-empty for a 2-signer tx", + ); + assert!( + affected.contains(&addr1), + "INVARIANT VIOLATION [AffectedAccountsContainSigners]: \ + affected_public_account_ids must include addr1 (signer)", + ); + assert!( + affected.contains(&addr2), + "INVARIANT VIOLATION [AffectedAccountsContainSigners]: \ + affected_public_account_ids must include addr2 (signer)", + ); + + // ── INVARIANT [HashNonDefault] ──────────────────────────────────────── + // The transaction hash must not be the all-zero default. + // Catches the mutation that returns `Default::default()`. + let lee_tx = LeeTransaction::Public(pub_tx); + let hash = lee_tx.hash(); + assert_ne!( + hash.0, + [0_u8; 32], + "INVARIANT VIOLATION [HashNonDefault]: \ + LeeTransaction::hash must not return all-zero bytes", + ); + + // Also verify it's deterministic (same tx → same hash): + let hash2 = lee_tx.hash(); + assert_eq!( + hash, + hash2, + "INVARIANT VIOLATION [HashDeterministic]: \ + LeeTransaction::hash must be deterministic", + ); + + // LeeTransaction::affected_public_account_ids must also be non-empty: + let lee_affected = lee_tx.affected_public_account_ids(); + assert!( + lee_affected.contains(&addr1), + "INVARIANT VIOLATION [AffectedAccountsContainSigners]: \ + LeeTransaction::affected_public_account_ids must include addr1", + ); + } + + // ── INVARIANT [SignerOnlyAccountInAffected] ─────────────────────────────── + // Build a transaction signed by a key whose AccountId is NOT among + // `message.account_ids`. Then `affected_public_account_ids` can only contain + // the signer's AccountId via `signer_account_ids()` — it is absent from the + // message's account list. This directly catches the `signer_account_ids` + // mutations (`→ vec![]` / `→ vec![Default::default()]`) on PublicTransaction, + // which the earlier checks miss because there the signer also appears in + // `message.account_ids`. + { + // Signer key — its AccountId must NOT appear in the message account list. + let signer_key = PrivateKey::try_new([9_u8; 32]).expect("known-good key"); + let signer_pub = PublicKey::new_from_private_key(&signer_key); + let signer_addr = AccountId::from(&signer_pub); + + // Two unrelated account IDs for the message (deterministic, not derived + // from the signer key). + let other1 = AccountId::new([0xA1_u8; 32]); + let other2 = AccountId::new([0xA2_u8; 32]); + + // Guard: ensure the signer is genuinely not one of the message accounts. + if signer_addr != other1 && signer_addr != other2 { + let nonces = vec![Nonce::from(0_u128)]; + if let Ok(msg) = Message::try_new( + Program::authenticated_transfer_program().id(), + vec![other1, other2], + nonces, + 7_u64, + ) { + let ws = WitnessSet::for_message(&msg, &[&signer_key]); + let pt = PublicTransaction::new(msg, ws); + + let affected = pt.affected_public_account_ids(); + assert!( + affected.contains(&signer_addr), + "INVARIANT VIOLATION [SignerOnlyAccountInAffected]: \ + affected_public_account_ids must include the signer {:?} even when it \ + is absent from message.account_ids — signer_account_ids() must not \ + return an empty (or defaulted) vec", + signer_addr, + ); + } + } + } + + // ── Part 2: Fuzz-driven state + valid native transfer ───────────────────── + // Generates a random state and a correctly-signed transfer. When the transfer + // succeeds, verifies that `public_diff` is non-empty and contains the + // expected account changes. + { + let fuzz_accs = match arbitrary_fuzz_state(&mut u) { + Ok(accs) => accs, + Err(_) => return, + }; + let init_accs: Vec<(AccountId, u128)> = fuzz_accs + .iter() + .map(|a| (a.account_id, a.balance)) + .collect(); + let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0); + + let Ok(tx) = arb_fuzz_native_transfer(&mut u, &fuzz_accs) else { + return; + }; + let Ok(checked) = tx.transaction_stateless_check() else { + return; + }; + + let pub_tx = match &checked { + LeeTransaction::Public(pt) => pt, + _ => return, + }; + + // For a public transaction with signers, affected_public_account_ids must + // include all signer account IDs. Derive signers from the public witness API. + let signers: Vec = pub_tx + .witness_set() + .signatures_and_public_keys() + .iter() + .map(|(_, pk)| AccountId::from(pk)) + .collect(); + let affected = pub_tx.affected_public_account_ids(); + for signer in &signers { + assert!( + affected.contains(signer), + "INVARIANT VIOLATION [AffectedAccountsContainSigners]: \ + affected_public_account_ids must include signer {:?}", + signer, + ); + } + + // When from_public_transaction succeeds, public_diff must be non-empty + // (at least the signer nonces are updated). + // Catches the mutation that returns `HashMap::new()`. + if let Ok(diff) = ValidatedStateDiff::from_public_transaction(pub_tx, &state, 1, 0) { + let public_diff = diff.public_diff(); + + // The diff must contain at least the signer accounts (nonce updates): + for signer in &signers { + // Signers appear in diff because their nonces are updated. + // If public_diff() returns empty HashMap, this assert fires. + assert!( + public_diff.contains_key(signer), + "INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \ + public_diff must contain signer account {:?} after successful validation \ + (nonce must have been updated)", + signer, + ); + } + } + } + + // ── Part 3: Fuzz-driven arbitrary keys for additional coverage ───────────── + { + if let Ok(key_wrap) = ArbPrivateKey::arbitrary(&mut u) { + let key = key_wrap.0; + let pubkey = PublicKey::new_from_private_key(&key); + let addr = AccountId::from(&pubkey); + + let nonces = vec![Nonce::from(0_u128)]; + if let Ok(msg) = Message::try_new( + Program::authenticated_transfer_program().id(), + vec![addr], + nonces, + 42_u64, + ) { + let ws = WitnessSet::for_message(&msg, &[&key]); + let pt = PublicTransaction::new(msg, ws); + + // Single-signer checks via witness_set (signer_account_ids is pub(crate)): + let ws_pairs2 = pt.witness_set().signatures_and_public_keys(); + assert_eq!( + ws_pairs2.len(), + 1, + "INVARIANT VIOLATION [SignerIdsNonEmpty]: 1-key witness set must have 1 pair", + ); + let derived_addr = AccountId::from(&ws_pairs2[0].1); + assert_eq!( + derived_addr, + addr, + "INVARIANT VIOLATION [SignerIdsDerivedFromKeys]: \ + derived signer address must match expected addr", + ); + + let ws2 = WitnessSet::for_message(pt.message(), &[&key]); + let parts = ws2.into_raw_parts(); + assert_eq!( + parts.len(), + 1, + "INVARIANT VIOLATION [IntoRawPartsCount]: 1-signer witness set → 1 raw part", + ); + } + } + } +}); diff --git a/scripts/mutants-corpus-test.sh b/scripts/mutants-corpus-test.sh index 0b409f17..bb04eee7 100755 --- a/scripts/mutants-corpus-test.sh +++ b/scripts/mutants-corpus-test.sh @@ -38,6 +38,12 @@ targets=( fuzz_sequencer_vs_replayer fuzz_merkle_tree fuzz_genesis_invariants + fuzz_common_invariants + fuzz_transaction_properties + fuzz_privacy_preserving_witness + fuzz_encoding_privacy_preserving + fuzz_nullifier_set_roundtrip + fuzz_system_account_protection ) # cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.). From 3c260eeef06c8721de575a3a8dd0cc4ce3720c0b Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 11 Jun 2026 15:43:31 +0800 Subject: [PATCH 22/31] fix: remove input-independent targets - create unit tests in lez repo instead --- docs/fuzzing.md | 34 ++++ fuzz/Cargo.toml | 18 -- fuzz/fuzz_targets/fuzz_common_invariants.rs | 171 ------------------ fuzz/fuzz_targets/fuzz_genesis_invariants.rs | 137 -------------- .../fuzz_system_account_protection.rs | 165 ----------------- scripts/mutants-corpus-test.sh | 3 - 6 files changed, 34 insertions(+), 494 deletions(-) delete mode 100644 fuzz/fuzz_targets/fuzz_common_invariants.rs delete mode 100644 fuzz/fuzz_targets/fuzz_genesis_invariants.rs delete mode 100644 fuzz/fuzz_targets/fuzz_system_account_protection.rs diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 3f8133b8..e8e1db5c 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -632,6 +632,40 @@ flag stubs out ZK proof generation and replaces it with a fast mock implementati --- +## Mutation testing — the two planes + +Mutation testing here runs in two distinct planes, answering two different questions: + +- **Plane A — "does a test catch this mutant?"** Run with a standard `cargo test` + oracle against the `lee` crate's own unit tests. +- **Plane B — "does the committed fuzz corpus catch this mutant?"** Run with + `just mutants-protocol`, which swaps `cargo test` for a fuzz-corpus replay + (`cargo fuzz run … -runs=0`) as the oracle. + +A mutant surviving Plane B is **not automatically a corpus gap to fill.** Some +mutations are only reachable by a fully-valid executing transaction or by a +deliberately-misbehaving program — neither of which a fuzzer can synthesise from +random bytes, and both of which are better pinned by deterministic unit tests in +the `lee` crate. Encoding such scenarios as input-independent fuzz targets only +duplicates those tests and slows every corpus replay. + +The mutants that are **expected** to survive Plane B (and where each is actually +covered) are catalogued in [`mutants-not-fuzzable.md`](mutants-not-fuzzable.md). +Reconcile new `mutants-protocol` runs against that list: only a surviving mutant +**not** on it warrants a new corpus input. + +**No input-independent targets.** A fuzz target whose closure ignores its input +(`|_data|`) is a deterministic unit test, not a fuzzer — it belongs in the LEZ +crate that owns the code. Three such targets once existed +(`fuzz_common_invariants`, `fuzz_genesis_invariants`, +`fuzz_system_account_protection`); their invariants were ported to LEZ unit tests +and the targets removed. The mutant→test mapping and verification are recorded in +[`input-independent-target-coverage.md`](input-independent-target-coverage.md). +When adding a target, drive it from `data`; if a check doesn't depend on the +input, write it as a unit test in `logos-execution-zone` instead. + +--- + ## Known Limitations & Future Work | Item | Notes | diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4b615487..d5219a56 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -126,18 +126,6 @@ path = "fuzz_targets/fuzz_merkle_tree.rs" test = false bench = false -[[bin]] -name = "fuzz_genesis_invariants" -path = "fuzz_targets/fuzz_genesis_invariants.rs" -test = false -bench = false - -[[bin]] -name = "fuzz_common_invariants" -path = "fuzz_targets/fuzz_common_invariants.rs" -test = false -bench = false - [[bin]] name = "fuzz_transaction_properties" path = "fuzz_targets/fuzz_transaction_properties.rs" @@ -161,9 +149,3 @@ name = "fuzz_nullifier_set_roundtrip" path = "fuzz_targets/fuzz_nullifier_set_roundtrip.rs" test = false bench = false - -[[bin]] -name = "fuzz_system_account_protection" -path = "fuzz_targets/fuzz_system_account_protection.rs" -test = false -bench = false diff --git a/fuzz/fuzz_targets/fuzz_common_invariants.rs b/fuzz/fuzz_targets/fuzz_common_invariants.rs deleted file mode 100644 index d4337ba2..00000000 --- a/fuzz/fuzz_targets/fuzz_common_invariants.rs +++ /dev/null @@ -1,171 +0,0 @@ -#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] -//! Fuzz target: common-crate and low-level type invariants. -//! -//! This target is **input-independent**: the fuzz input is always ignored. -//! It asserts deterministic invariants about types in `lez/common` and -//! low-level `lee` types that are not exercised by higher-level state-transition -//! targets. -//! -//! # Corpus note -//! -//! A single `\x00` seed file is sufficient — the input bytes are never read. - -use common::{HashType, config::BasicAuth}; -use nssa::{ - privacy_preserving_transaction::circuit::Proof, - program::Program, - program_deployment_transaction::Message as DeployMessage, - program_methods::{ - AUTHENTICATED_TRANSFER_ELF, TOKEN_ELF, - }, -}; - -fuzz_props::fuzz_entry!(|_data: &[u8]| { - // ── INVARIANT [HashTypeAsRefLength] ──────────────────────────────────────── - // `HashType::as_ref()` must always return exactly 32 bytes. - // Catches mutations that return an empty slice or a slice of the wrong size. - let all_ones = HashType([1_u8; 32]); - assert_eq!( - all_ones.as_ref().len(), - 32, - "INVARIANT VIOLATION [HashTypeAsRefLength]: HashType::as_ref must return 32 bytes", - ); - - let zero = HashType::default(); - assert_eq!( - zero.as_ref().len(), - 32, - "INVARIANT VIOLATION [HashTypeAsRefLength]: HashType::as_ref on default must return 32 bytes", - ); - - // ── INVARIANT [HashTypeAsRefBytes] ──────────────────────────────────────── - // `HashType::as_ref()` must return the exact inner bytes. - // Catches mutations that return `vec![0]` or `vec![1]` instead of `&self.0`. - let known = [0x42_u8; 32]; - let hash = HashType(known); - assert_eq!( - hash.as_ref(), - &known, - "INVARIANT VIOLATION [HashTypeAsRefBytes]: HashType::as_ref must return the inner [u8;32]", - ); - - // ── INVARIANT [BasicAuthPasswordPreserved] ─────────────────────────────── - // Parsing "user:password" must preserve the non-empty password as `Some`. - // Catches the mutation that deletes `!` in the `.filter(|p| !p.is_empty())` - // predicate, which would flip the logic and accept only empty passwords. - let auth: BasicAuth = "user:secret" - .parse() - .expect("INVARIANT VIOLATION: 'user:secret' must parse as BasicAuth"); - assert_eq!( - auth.password.as_deref(), - Some("secret"), - "INVARIANT VIOLATION [BasicAuthPasswordPreserved]: \ - parsing 'user:secret' must give password = Some(\"secret\")", - ); - - let auth2: BasicAuth = "alice:hunter2" - .parse() - .expect("INVARIANT VIOLATION: 'alice:hunter2' must parse"); - assert_eq!( - auth2.password.as_deref(), - Some("hunter2"), - "INVARIANT VIOLATION [BasicAuthPasswordPreserved]: \ - password must match the part after the colon", - ); - - // ── INVARIANT [BasicAuthEmptyPasswordIsNone] ───────────────────────────── - // Parsing "user:" (empty password) must give `password = None`. - // With the `!` deleted, this would become `Some("")` instead of `None`. - let auth_empty: BasicAuth = "user:" - .parse() - .expect("INVARIANT VIOLATION: 'user:' must parse as BasicAuth"); - assert_eq!( - auth_empty.password, - None, - "INVARIANT VIOLATION [BasicAuthEmptyPasswordIsNone]: \ - an empty password (trailing colon) must give password = None", - ); - - // ── INVARIANT [ProgramElfNonEmpty] ─────────────────────────────────────── - // `Program::elf()` must return a non-empty byte slice. - // Catches the mutation that returns `Vec::leak(Vec::new())`. - let at_prog = Program::authenticated_transfer_program(); - assert!( - !at_prog.elf().is_empty(), - "INVARIANT VIOLATION [ProgramElfNonEmpty]: \ - Program::authenticated_transfer_program().elf() must not be empty", - ); - - let token_prog = Program::token(); - assert!( - !token_prog.elf().is_empty(), - "INVARIANT VIOLATION [ProgramElfNonEmpty]: \ - Program::token().elf() must not be empty", - ); - - // ── INVARIANT [ProgramElfCorrect] ──────────────────────────────────────── - // `Program::elf()` must return exactly the compile-time bytecode constant. - // Catches the mutations that return `vec![0]` or `vec![1]`. - assert_eq!( - at_prog.elf(), - AUTHENTICATED_TRANSFER_ELF, - "INVARIANT VIOLATION [ProgramElfCorrect]: \ - Program::authenticated_transfer_program().elf() must equal AUTHENTICATED_TRANSFER_ELF", - ); - - assert_eq!( - token_prog.elf(), - TOKEN_ELF, - "INVARIANT VIOLATION [ProgramElfCorrect]: \ - Program::token().elf() must equal TOKEN_ELF", - ); - - // ── INVARIANT [ProofIntoInnerRoundtrip] ────────────────────────────────── - // `Proof::from_inner(bytes).into_inner()` must return the original bytes. - // Catches the mutations that return `vec![]`, `vec![0]`, or `vec![1]`. - let proof_bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF]; - let proof = Proof::from_inner(proof_bytes.clone()); - assert_eq!( - proof.into_inner(), - proof_bytes, - "INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \ - Proof::from_inner(b).into_inner() must return b", - ); - - // Also test with an empty proof (round-trip must preserve emptiness). - let empty_proof = Proof::from_inner(vec![]); - assert!( - empty_proof.into_inner().is_empty(), - "INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \ - empty Proof::from_inner(vec![]).into_inner() must be empty", - ); - - // And with a single non-zero byte: - let single = Proof::from_inner(vec![0xFF]); - assert_eq!( - single.into_inner(), - vec![0xFF_u8], - "INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \ - Proof from single byte must round-trip correctly", - ); - - // ── INVARIANT [DeployMessageBytecodeRoundtrip] ──────────────────────────── - // `Message::new(bytecode).into_bytecode()` must return the original bytecode. - // Catches the mutations that return `vec![]`, `vec![0]`, or `vec![1]`. - let bytecode = vec![0x7F_u8, 0x45, 0x4C, 0x46]; // ELF magic - let msg = DeployMessage::new(bytecode.clone()); - assert_eq!( - msg.into_bytecode(), - bytecode, - "INVARIANT VIOLATION [DeployMessageBytecodeRoundtrip]: \ - Message::new(b).into_bytecode() must return b", - ); - - // Empty bytecode round-trip: - let empty_msg = DeployMessage::new(vec![]); - assert!( - empty_msg.into_bytecode().is_empty(), - "INVARIANT VIOLATION [DeployMessageBytecodeRoundtrip]: \ - empty bytecode must round-trip as empty", - ); -}); diff --git a/fuzz/fuzz_targets/fuzz_genesis_invariants.rs b/fuzz/fuzz_targets/fuzz_genesis_invariants.rs deleted file mode 100644 index fafb53b3..00000000 --- a/fuzz/fuzz_targets/fuzz_genesis_invariants.rs +++ /dev/null @@ -1,137 +0,0 @@ -#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] -//! Fuzz target: genesis-state and system-account invariants. -//! -//! This target is **input-independent**: the fuzz input is always ignored. -//! It asserts deterministic invariants about the genesis state produced by -//! `V03State::new_with_genesis_accounts`, `system_faucet_account_id`, -//! `system_bridge_account_id`, and `V03State::add_pinata_token_program`. -//! -//! # Covered mutations (from `lee/state_machine/src/state.rs`) -//! -//! | Line | Mutation | Assertion that catches it | -//! |------|--------------------------------------------------------|-----------------------------------------------------| -//! | 312 | `commitment_set_digest → Default::default()` | `[CommitmentSetDigestNonDefault]` | -//! | 368 | delete `program_owner` from `add_pinata_token_program` | `[PinataTokenProgramOwner]` | -//! | 370 | delete `data` from `add_pinata_token_program` | `[PinataTokenData]` | -//! | 385 | `system_faucet_account → Default::default()` | `[FaucetBalance]` + `[FaucetProgramOwner]` | -//! | 386 | delete `program_owner` from `system_faucet_account` | `[FaucetProgramOwner]` | -//! | 387 | delete `balance` from `system_faucet_account` | `[FaucetBalance]` | -//! | 393 | `system_bridge_account → Default::default()` | `[BridgeProgramOwner]` | -//! | 394 | delete `program_owner` from `system_bridge_account` | `[BridgeProgramOwner]` | -//! | 406 | `system_bridge_account_id → Default::default()` | `[BridgeIdNonDefault]` + `[SystemAccountIdDistinct]` | -//! -//! # Corpus note -//! -//! A single `\x00` seed file is sufficient — the input bytes are never read. -//! The seed is required by `cargo fuzz run -runs=0` so that the replay phase -//! has at least one execution to check against. - -use nssa::{Account, AccountId, V03State, system_bridge_account_id, system_faucet_account_id}; - -fuzz_props::fuzz_entry!(|_data: &[u8]| { - let default_account = Account::default(); - - // ── INVARIANT [BridgeIdNonDefault] ──────────────────────────────────────── - // `system_bridge_account_id()` must return a non-default `AccountId`. - // Catches the mutation at state.rs:406 that replaces the function body with - // `Default::default()`. - let bridge_id = system_bridge_account_id(); - assert_ne!( - bridge_id, - AccountId::default(), - "INVARIANT VIOLATION [BridgeIdNonDefault]: \ - system_bridge_account_id() must not return AccountId::default()", - ); - - // The two system account IDs must also be distinct so that they occupy - // separate entries in the public-state map. - let faucet_id = system_faucet_account_id(); - assert_ne!( - faucet_id, - bridge_id, - "INVARIANT VIOLATION [SystemAccountIdDistinct]: \ - system_faucet_account_id() and system_bridge_account_id() must differ", - ); - - // Build the genesis state with no extra accounts. - let state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - // ── INVARIANT [FaucetBalance] ───────────────────────────────────────────── - // The system faucet account must hold `u128::MAX` tokens. - // Catches state.rs:385 (whole account → Default) and - // state.rs:387 (delete `balance` field from struct literal). - let faucet = state.get_account_by_id(faucet_id); - assert_eq!( - faucet.balance, - u128::MAX, - "INVARIANT VIOLATION [FaucetBalance]: \ - system_faucet_account must have balance == u128::MAX, got {}", - faucet.balance, - ); - - // ── INVARIANT [FaucetProgramOwner] ──────────────────────────────────────── - // The system faucet account must have a non-default `program_owner`. - // Catches state.rs:385 (whole account → Default) and - // state.rs:386 (delete `program_owner` field from struct literal). - assert_ne!( - faucet.program_owner, - default_account.program_owner, - "INVARIANT VIOLATION [FaucetProgramOwner]: \ - system_faucet_account must have a non-default program_owner", - ); - - // ── INVARIANT [BridgeProgramOwner] ─────────────────────────────────────── - // The system bridge account must have a non-default `program_owner`. - // Catches state.rs:393 (whole account → Default) and - // state.rs:394 (delete `program_owner` field from struct literal). - let bridge = state.get_account_by_id(bridge_id); - assert_ne!( - bridge.program_owner, - default_account.program_owner, - "INVARIANT VIOLATION [BridgeProgramOwner]: \ - system_bridge_account must have a non-default program_owner", - ); - - // ── INVARIANT [CommitmentSetDigestNonDefault] ───────────────────────────── - // A freshly created empty state has an all-zero Merkle root, which equals - // `CommitmentSetDigest::default()`. The genesis state inserts - // `DUMMY_COMMITMENT` via SHA-256, producing a strictly different root. - // Catches state.rs:312 that replaces `commitment_set_digest()` with - // `Default::default()`. - let empty_digest = V03State::new().commitment_set_digest(); - let genesis_digest = state.commitment_set_digest(); - assert_ne!( - genesis_digest, - empty_digest, - "INVARIANT VIOLATION [CommitmentSetDigestNonDefault]: \ - commitment_set_digest of genesis state must differ from the empty state's \ - all-zero root", - ); - - // ── INVARIANT [PinataTokenProgramOwner] ────────────────────────────────── - // An account created by `add_pinata_token_program` must have a non-default - // `program_owner` field. - // Catches state.rs:368 (delete `program_owner` from the struct literal). - // - // ── INVARIANT [PinataTokenData] ────────────────────────────────────────── - // An account created by `add_pinata_token_program` must have non-default - // `data` (specifically `vec![3; 33]` encoded as `Data`). - // Catches state.rs:370 (delete `data` from the struct literal). - let pt_id = AccountId::new([0xABu8; 32]); - let mut pinata_state = V03State::new_with_genesis_accounts(&[], vec![], 0); - pinata_state.add_pinata_token_program(pt_id); - let pt = pinata_state.get_account_by_id(pt_id); - - assert_ne!( - pt.program_owner, - default_account.program_owner, - "INVARIANT VIOLATION [PinataTokenProgramOwner]: \ - add_pinata_token_program must set a non-default program_owner on the account", - ); - assert_ne!( - pt.data, - default_account.data, - "INVARIANT VIOLATION [PinataTokenData]: \ - add_pinata_token_program must set non-default data on the account", - ); -}); diff --git a/fuzz/fuzz_targets/fuzz_system_account_protection.rs b/fuzz/fuzz_targets/fuzz_system_account_protection.rs deleted file mode 100644 index 8555b92b..00000000 --- a/fuzz/fuzz_targets/fuzz_system_account_protection.rs +++ /dev/null @@ -1,165 +0,0 @@ -#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] -//! Fuzz target: system-account modification protection. -//! -//! `LeeTransaction::validate_on_state` must reject any transaction that modifies -//! a system account (faucet, bridge, or clock accounts). This is enforced by -//! `validate_doesnt_modify_account` which inspects `ValidatedStateDiff::public_diff()`. -//! -//! # Corpus note -//! -//! This target is **input-independent**. A single `\x00` seed is sufficient. -//! -//! **Performance note**: the `[SystemAccountModificationRejected]` invariant -//! executes a RISC0 program (a native transfer). This is inherently slow -//! (~seconds). Only one corpus file is needed, so the corpus-regression oracle -//! costs one program execution per mutant under test. - -use common::transaction::LeeTransaction; -use nssa::{ - AccountId, PrivateKey, PublicKey, V03State, ValidatedStateDiff, - CLOCK_01_PROGRAM_ACCOUNT_ID, system_bridge_account_id, system_faucet_account_id, -}; - -fuzz_props::fuzz_entry!(|_data: &[u8]| { - // ── INVARIANT [SystemAccountIdsDistinct] ────────────────────────────────── - let faucet_id = system_faucet_account_id(); - let bridge_id = system_bridge_account_id(); - - assert_ne!( - faucet_id, - AccountId::default(), - "INVARIANT VIOLATION [SystemAccountIdsDistinct]: faucet account ID must be non-default", - ); - assert_ne!( - bridge_id, - AccountId::default(), - "INVARIANT VIOLATION [SystemAccountIdsDistinct]: bridge account ID must be non-default", - ); - assert_ne!( - faucet_id, - bridge_id, - "INVARIANT VIOLATION [SystemAccountIdsDistinct]: faucet and bridge must be distinct", - ); - - // ── INVARIANT [ClockInvocationRejected] ────────────────────────────────── - // A native transfer that CREDITS a clock system account modifies exactly one - // system account — the clock account — and that account is *changed* (its - // balance increases from 0). No other system account appears in the diff, so - // this isolates the `validate_doesnt_modify_account` rejection cleanly. - // - // Why not a clock invocation? The clock program writes all three clock - // accounts, but the 01/10/50 clocks tick at different rates, so its diff - // contains BOTH changed and unchanged system accounts. The `!=`→`==` - // mutation then still rejects (citing an unchanged account), so a clock - // invocation cannot distinguish the mutant. Crediting a single clock account - // gives a single, changed system account, which the mutant must accept. - // - // Why not credit the faucet? The faucet holds u128::MAX, so any credit - // overflows and the program execution fails before the protection check is - // reached. A clock account starts at balance 0, so a small credit succeeds. - // - // With mutation `!=` → `==` at transaction.rs:182: - // The clock account is changed (post != pre), so the mutated `post == pre` - // check is false → no error → validate_on_state returns Ok → our assert fires. - // - // With mutation `public_diff → HashMap::new()` at validated_state_diff.rs:479: - // validate_doesnt_modify_account sees an empty map → can never find the - // clock account → returns Ok for every transaction → our assert fires. - { - let sender_key = PrivateKey::try_new([5_u8; 32]).expect("known-good key"); - let sender_pub = PublicKey::new_from_private_key(&sender_key); - let sender_id = AccountId::from(&sender_pub); - - // Fund the sender; clock accounts already exist in genesis (balance 0). - let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000_u128)], vec![], 0); - - // Transfer tokens TO a clock account — credits (changes) that system account. - let tx = common::test_utils::create_transaction_native_token_transfer( - sender_id, - 0, // nonce - CLOCK_01_PROGRAM_ACCOUNT_ID, - 100, // amount credited to the clock account - &sender_key, - ); - - let result = tx.validate_on_state(&state, 1, 0); - - assert!( - result.is_err(), - "INVARIANT VIOLATION [SystemAccountModificationRejected]: \ - validate_on_state must reject a transfer that credits a clock system \ - account. If this fires, either validate_doesnt_modify_account has a logic \ - inversion (!=→==) or public_diff() returns an empty map", - ); - } - - // ── INVARIANT [PublicDiffNonEmptyOnSuccess] ──────────────────────────────── - // For a valid public transaction with signers, the signer accounts must appear - // in public_diff after successful validation (nonces are updated in the diff). - // - // With mutation `public_diff → HashMap::new()`: - // The map is empty → `contains_key(&signer)` returns false → assert fires. - // - // Uses `common::test_utils::create_transaction_native_token_transfer` to - // construct a semantically valid transaction (correct instruction type). - { - let key = PrivateKey::try_new([7_u8; 32]).expect("known-good key"); - let pubkey = PublicKey::new_from_private_key(&key); - let addr = AccountId::from(&pubkey); - - let key2 = PrivateKey::try_new([8_u8; 32]).expect("known-good key"); - let pubkey2 = PublicKey::new_from_private_key(&key2); - let addr2 = AccountId::from(&pubkey2); - - let state = V03State::new_with_genesis_accounts( - &[(addr, 10_000_u128), (addr2, 10_000_u128)], - vec![], - 0, - ); - - // Use the test utility to build a valid native token transfer. - // This uses the correct authenticated_transfer_core::Instruction::Transfer, - // which the program can actually execute without panicking. - let lee_tx = common::test_utils::create_transaction_native_token_transfer( - addr, - 0, // nonce = 0 (matches initial state nonce) - addr2, - 100, // amount - &key, - ); - - if let LeeTransaction::Public(pub_tx) = &lee_tx { - if let Ok(diff) = ValidatedStateDiff::from_public_transaction(&pub_tx, &state, 1, 0) { - let public_diff = diff.public_diff(); - - // The signer/sender (addr) must be in the diff: a native transfer - // debits its balance, so it MUST appear in public_diff. - // If public_diff() returns an empty HashMap, this assert fires. - // - // Note: nonce increments are applied separately during - // `apply_state_diff` via `signer_account_ids` and are NOT recorded - // in `public_diff`, so we do not assert on the nonce field here. - assert!( - public_diff.contains_key(&addr), - "INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \ - public_diff must contain the sender {:?} (its balance is debited) \ - after a successful native transfer \ - (mutation public_diff→HashMap::new() detected)", - addr, - ); - - // The diff must reflect the balance debit on the sender — the - // balance recorded in the diff must differ from the pre-state. - let pre_balance = state.get_account_by_id(addr).balance; - let post_balance = public_diff[&addr].balance; - assert_ne!( - post_balance, - pre_balance, - "INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \ - sender balance in the diff must differ from pre-state after a transfer \ - (pre={pre_balance}, post={post_balance})", - ); - } - } - } -}); diff --git a/scripts/mutants-corpus-test.sh b/scripts/mutants-corpus-test.sh index bb04eee7..b1409bae 100755 --- a/scripts/mutants-corpus-test.sh +++ b/scripts/mutants-corpus-test.sh @@ -37,13 +37,10 @@ targets=( fuzz_multi_block_state_sequence fuzz_sequencer_vs_replayer fuzz_merkle_tree - fuzz_genesis_invariants - fuzz_common_invariants fuzz_transaction_properties fuzz_privacy_preserving_witness fuzz_encoding_privacy_preserving fuzz_nullifier_set_roundtrip - fuzz_system_account_protection ) # cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.). From 2659d390eb40de0d48d37cbdbd00cb7f5f9c66b9 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jun 2026 09:07:10 +0800 Subject: [PATCH 23/31] fix: update documentation --- README.md | 53 +++++-- current_vs_alternative_approach.md | 45 ++++-- docs/fuzzing.md | 8 +- docs/mutants-not-fuzzable.md | 231 +++++++++++++++++++++++++++++ 4 files changed, 310 insertions(+), 27 deletions(-) create mode 100644 docs/mutants-not-fuzzable.md diff --git a/README.md b/README.md index 082fa38c..bc5b4a92 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,20 @@ lez-fuzzing/ │ │ ├── fuzz_witness_set_verification.rs │ │ ├── fuzz_program_deployment_lifecycle.rs │ │ ├── fuzz_apply_state_diff_split_path.rs -│ │ └── fuzz_multi_block_state_sequence.rs +│ │ ├── fuzz_multi_block_state_sequence.rs +│ │ ├── fuzz_sequencer_vs_replayer.rs +│ │ ├── fuzz_merkle_tree.rs +│ │ ├── fuzz_transaction_properties.rs +│ │ ├── fuzz_privacy_preserving_witness.rs +│ │ ├── fuzz_encoding_privacy_preserving.rs +│ │ └── fuzz_nullifier_set_roundtrip.rs # 20 targets total — see table below │ └── corpus/ # Curated seed inputs (one dir per target) ├── .github/ │ └── workflows/ -│ └── fuzz.yml # CI: smoke-fuzz · regression · proptest · perf +│ ├── fuzz.yml # CI: smoke-fuzz · regression · proptest · perf (libFuzzer) +│ ├── fuzz-afl.yml # CI: AFL++ lane +│ ├── mutants.yml # CI: mutation testing (cargo-mutants) +│ └── lint.yml # CI: fmt + clippy ├── scripts/ │ └── add_fuzz_target.py # Automates new-target scaffolding (called by just new-target) └── docs/ @@ -130,6 +139,19 @@ just fuzz-props | `fuzz_program_deployment_lifecycle` | `V03State::transition_from_program_deployment_transaction` no-panic + BalanceIsolation (deployment must not move tokens) + StateIsolationOnFailure | `fuzz/fuzz_targets/fuzz_program_deployment_lifecycle.rs` | | `fuzz_apply_state_diff_split_path` | SplitPathEquivalence: `validate_on_state + apply_state_diff` == `execute_check_on_state` for all known accounts (balance, nonce, data, program_owner); NonceIncrementCorrectness | `fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs` | | `fuzz_multi_block_state_sequence` | LongRangeBalanceConservation across up to 16 blocks + FailedTxNonceStability (nonce must not change on rejection) + PerBlockReplayRejection | `fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs` | +| `fuzz_sequencer_vs_replayer` | Differential: sequencer path (`validate_on_state` → `apply_state_diff`) vs replayer path (`execute_check_on_state`) — SequencerReplayerEquivalence + ReplayerAcceptsAllSequencerTxs + ClockConsistency | `fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs` | +| `fuzz_merkle_tree` | Commitment Merkle tree via the commitment set: ProofSome · ProofValid (leaf + auth path recomputes the root) · NonMembershipNone · IndicesSequential | `fuzz/fuzz_targets/fuzz_merkle_tree.rs` | +| `fuzz_transaction_properties` | Transaction property invariants: HashDeterministic/HashNonDefault, SignerIds derived from witness keys & non-empty, AffectedAccountsContainSigners, PublicDiffNonEmptyOnSuccess | `fuzz/fuzz_targets/fuzz_transaction_properties.rs` | +| `fuzz_privacy_preserving_witness` | `privacy_preserving_transaction::WitnessSet`: CorrectVerification (witness for msg A passes `signatures_are_valid_for(A)`) + MessageIsolation + SignerIdsMatchWitnessKeys | `fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs` | +| `fuzz_encoding_privacy_preserving` | Privacy-preserving encoding: MessageEncodingRoundtrip + TxEncodingDeterministic/NonEmpty | `fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs` | +| `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: NullifierSetRoundtrip (decode→encode identity for the hand-written impl) | `fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs` | + +> **Input-independent checks are not fuzz targets here.** Deterministic invariants +> that ignore their input (e.g. genesis-account contents, getter/round-trip +> identities, the system-account-modification guard) belong in `logos-execution-zone` +> unit tests, not the fuzz corpus. See +> [`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md) for the policy and +> the mutant→test mapping. --- @@ -187,20 +209,23 @@ just clean-all # All of the above ## CI -GitHub Actions runs four jobs on every push/PR and nightly: +GitHub Actions runs these workflows on every push/PR and nightly: -| Job | What it does | -|-----|-------------| -| `smoke-fuzz` (matrix, 9 targets) | Builds + runs each target for 60 s | -| `regression` (matrix, 9 targets) | Replays the saved corpus (`-runs=0`) | -| `proptest` | `cargo test -p fuzz_props --release` | -| `perf-baseline` (nightly only) | Measures exec/sec per target, uploads `perf_baseline.txt` | +| Workflow | What it does | +|----------|-------------| +| `fuzz.yml` — `smoke-fuzz` (matrix) | Builds + runs each libFuzzer target for 60 s | +| `fuzz.yml` — `regression` (matrix) | Replays the saved corpus (`-runs=0`) | +| `fuzz.yml` — `proptest` | `cargo test -p fuzz_props --release` | +| `fuzz.yml` — `perf-baseline` (nightly only) | Measures exec/sec per target, uploads `perf_baseline.txt` | +| `fuzz-afl.yml` | AFL++ lane over the same targets/corpus | +| `mutants.yml` | Mutation testing (`cargo-mutants`) | +| `lint.yml` | Formatting + Clippy | -> **Note:** The CI matrix currently lists the original 9 targets. The 5 new targets -> (`fuzz_state_serialization`, `fuzz_witness_set_verification`, -> `fuzz_program_deployment_lifecycle`, `fuzz_apply_state_diff_split_path`, -> `fuzz_multi_block_state_sequence`) need to be added to `.github/workflows/fuzz.yml` -> — see [`docs/fuzzing.md`](docs/fuzzing.md) for the manual fallback instructions. +> **Note:** The `fuzz.yml` matrix currently lists 15 of the 20 libFuzzer targets. +> Still missing: `fuzz_merkle_tree`, `fuzz_transaction_properties`, +> `fuzz_privacy_preserving_witness`, `fuzz_encoding_privacy_preserving`, and +> `fuzz_nullifier_set_roundtrip` — add them to `.github/workflows/fuzz.yml`. See +> [`docs/fuzzing.md`](docs/fuzzing.md) for the manual fallback instructions. --- diff --git a/current_vs_alternative_approach.md b/current_vs_alternative_approach.md index 8498316c..ba97b255 100644 --- a/current_vs_alternative_approach.md +++ b/current_vs_alternative_approach.md @@ -11,8 +11,8 @@ 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 | -| 15 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 | -| CI integration | GitHub Actions smoke, regression, and performance-baseline jobs run on every PR | +| 20 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, and nullifier-set round-trips. 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 | --- @@ -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:355) 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**: ✅ **Implemented.** AFL++ and libFuzzer find *different* bugs because they use different mutation heuristics, and running both on the same corpus is the industry-standard "belt and suspenders" approach. AFL++ is now a live lane: `just fuzz-afl` / `just fuzz-afl-parallel` and the `.github/workflows/fuzz-afl.yml` nightly job, sharing the same [`fuzz_props`](fuzz_props/src/lib.rs) crate and seed corpus at **zero migration cost**. --- @@ -111,7 +111,19 @@ The extension noted in [`docs/fuzzing.md`](docs/fuzzing.md:356) 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) has gaps. Three invariants are fully implemented and registered in `assert_invariants()`: [`StateIsolationOnFailure`](fuzz_props/src/invariants.rs:60), [`BalanceConservation`](fuzz_props/src/invariants.rs:94), and [`FailedTxNonceStability`](fuzz_props/src/invariants.rs:130). Two additional invariants — [`ReplayRejection`](fuzz_props/src/invariants.rs:167) and [`NonceIncrementCorrectness`](fuzz_props/src/invariants.rs:194) — are enforced exclusively via standalone helpers (`assert_replay_rejection`, `assert_nonce_increment_correctness`) and are **not** in the `assert_invariants()` registry; this is intentional because they require data consumed before `InvariantCtx` is built. This is a **complementary quality gate**, not a fuzzing replacement. Low cost (~1 day), highly useful before an external security audit. +**Decision-maker view**: ✅ **Implemented.** `cargo-mutants` runs in two modes — +`just mutants-harness` (mutates `fuzz_props`, oracle = `cargo test`, auditing the +invariant assertions themselves) and `just mutants-protocol` (mutates the LEZ +`lee`/`common` crates, oracle = a fuzz-corpus replay), with a `mutants.yml` CI job. +The two oracles correspond to a deliberate **Plane A / Plane B** split — see +[`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md), which catalogues +the mutants each plane is and isn't expected to catch and why. (For reference, the +`fuzz_props` registry still implements [`StateIsolationOnFailure`](fuzz_props/src/invariants.rs), +[`BalanceConservation`](fuzz_props/src/invariants.rs), and +[`FailedTxNonceStability`](fuzz_props/src/invariants.rs) in `assert_invariants()`, +with `ReplayRejection` and `NonceIncrementCorrectness` enforced via standalone +helpers outside the registry.) This is a **complementary quality gate**, not a +fuzzing replacement. --- @@ -120,19 +132,34 @@ The extension noted in [`docs/fuzzing.md`](docs/fuzzing.md:356) is: | Approach | Bug-finding depth | CI cost | Impl. cost | Complements current? | Recommended action | |---|---|---|---|---|---| | **Current (cargo-fuzz/libFuzzer)** | High | Medium | ✅ Done | — | Maintain & expand | -| AFL++ | High (different bugs) | Medium | Low | ✅ Yes | Add `just fuzz-afl` (already planned) | +| AFL++ | High (different bugs) | Medium | ✅ Done | ✅ Yes | ✅ Implemented (`just fuzz-afl`, `fuzz-afl.yml`) | | Honggfuzz | High on Linux | Medium | Medium | ✅ Yes | Add for Linux CI only | | proptest-only | Low–medium | Low | ✅ Done | Already present | Keep as unit-test layer | | Differential (sequencer/replayer) | Very high (new bug class) | Medium | ✅ Done | ✅ Yes | ✅ Implemented (`fuzz_sequencer_vs_replayer`) | | Formal verification | Exhaustive (selected invariants) | Very high | Very high | ✅ Yes | Long-term supplement | -| Mutation testing (`cargo-mutants`) | Measures assertion quality | High | Low | ✅ Yes | Pre-audit quality gate | +| Mutation testing (`cargo-mutants`) | Measures assertion quality | High | ✅ Done | ✅ Yes | ✅ Implemented (`just mutants-harness` / `mutants-protocol`) | --- ## Decision-maker Recommendations -**Highest-ROI next steps, in priority order:** +**Already done** (was previously recommended here): -1. **Add AFL++ as a parallel fuzzing lane** (`just fuzz-afl`) — zero corpus migration cost, discovers different mutation paths through the same targets as libFuzzer. +- ✅ **AFL++ parallel lane** — `just fuzz-afl` + `fuzz-afl.yml`. +- ✅ **`cargo-mutants`** — `just mutants-harness` / `mutants-protocol` + `mutants.yml`, + with the Plane A / Plane B framework documented in + [`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md). +- ✅ **Differential testing** — `fuzz_sequencer_vs_replayer`. -2. **Add `cargo-mutants`** before any external security audit — proves the invariant assertions in [`fuzz_props/src/invariants.rs`](fuzz_props/src/invariants.rs) are actually capable of catching the bugs they claim to detect. \ No newline at end of file +**Remaining higher-ROI next steps, in priority order:** + +1. **Finish the `fuzz.yml` CI matrix** — it lists 15 of the 20 libFuzzer targets; + add `fuzz_merkle_tree`, `fuzz_transaction_properties`, + `fuzz_privacy_preserving_witness`, `fuzz_encoding_privacy_preserving`, and + `fuzz_nullifier_set_roundtrip`. + +2. **Honggfuzz on Linux CI only** — hardware-counter coverage finds different paths; + gated to Linux since Apple Silicon has no HW counters. + +3. **Formal verification of core invariants** (balance conservation, replay + prevention) — a long-term supplement, not a replacement. \ No newline at end of file diff --git a/docs/fuzzing.md b/docs/fuzzing.md index e8e1db5c..5c3579ef 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -659,10 +659,10 @@ Reconcile new `mutants-protocol` runs against that list: only a surviving mutant crate that owns the code. Three such targets once existed (`fuzz_common_invariants`, `fuzz_genesis_invariants`, `fuzz_system_account_protection`); their invariants were ported to LEZ unit tests -and the targets removed. The mutant→test mapping and verification are recorded in -[`input-independent-target-coverage.md`](input-independent-target-coverage.md). -When adding a target, drive it from `data`; if a check doesn't depend on the -input, write it as a unit test in `logos-execution-zone` instead. +and the targets removed. The mutant→test mapping is recorded under "Group 2" in +[`mutants-not-fuzzable.md`](mutants-not-fuzzable.md). When adding a target, drive it +from `data`; if a check doesn't depend on the input, write it as a unit test in +`logos-execution-zone` instead. --- diff --git a/docs/mutants-not-fuzzable.md b/docs/mutants-not-fuzzable.md new file mode 100644 index 00000000..0efdf031 --- /dev/null +++ b/docs/mutants-not-fuzzable.md @@ -0,0 +1,231 @@ +# Mutants Not Coverable by Fuzzing + +This document catalogues the source mutations (from `just mutants-protocol`, the +"Plane B" corpus-replay mutation run over the `lee` / `common` crates) that the +**fuzzing corpus is not the right tool to catch**, together with where each one is +actually covered. + +It exists to keep a clean separation between two questions that the tooling can +otherwise blur together: + +- **"Does a test catch this mutant?"** — answered by the `lee` crate's own unit + tests via `cargo test` (call this **Plane A**). +- **"Does the committed fuzz corpus catch this mutant?"** — answered by + `just mutants-protocol`, which replaces `cargo test` with a fuzz-corpus replay + (`cargo fuzz run … -runs=0`) as the oracle (call this **Plane B**). + +The mutants listed here are **expected Plane-B misses**. A future +`mutants-protocol` run that reports them as surviving is *not* a regression — it +is the documented, intended state. + +This file is the complete registry, in **two groups**: + +1. **Structurally unreachable by fuzzing** (Group 1) — mutants behind code that a + fuzzer cannot reach from raw bytes (they need a valid executing transaction or a + deliberately-misbehaving program). These were always unit-test territory. +2. **Migrated input-independent targets** (Group 2) — mutants that *were* caught by + input-independent fuzz targets (`fuzz_common_invariants`, + `fuzz_genesis_invariants`, `fuzz_system_account_protection`). Because an + input-independent target is a unit test in disguise, those targets were removed + and their invariants ported to LEZ unit tests; the mutants therefore now survive + Plane B by design. + +Reconcile new `mutants-protocol` runs against this registry; only a surviving +mutant on **neither** list warrants a new corpus input. + +--- + +## Why fuzzing is the wrong tool for these + +Fuzzing earns its keep by exploring a large, *unknown* input space to find inputs +a human wouldn't think of — malformed transactions, adversarial byte sequences, +surprising state-transition orderings. The corpus-replay oracle then re-runs those +discovered inputs cheaply as a regression net. + +The mutations below live behind code that is only reachable by a **specific, +valid, semantically rich object** that random bytes essentially never synthesise: + +1. **A fully-valid, executing transaction.** Reaching the post-execution + validation logic (authorization checks, claim checks, cycle limit) requires a + transaction whose signature matches its signer, whose nonce matches the + on-chain nonce, and whose program is deployed. A fuzzer mutating raw bytes + almost always breaks one of these and is rejected at the stateless/nonce gate + *before* any program runs — so the code never executes. Constructing such a + transaction is a deterministic "this exact scenario must hold" property, which + is the domain of **unit tests**, not input exploration. + +2. **A deliberately-misbehaving program.** Some validator checks only fire when a + program returns malformed output (claims an account it shouldn't, mutates a + default account without claiming it, etc.). The only such programs are the + test fixtures behind `V03State::with_test_programs()` (`program_owner_changer`, + `extra_output_program`, …). They are **never deployed** in genesis or + production, so they are unreachable through the public transaction API that the + fuzzer drives — by construction, no fuzz input can exercise them. + +In both cases the behaviour is pinned by deterministic unit tests in the `lee` / +`common` crates. Encoding such scenarios as **input-independent** fuzz targets +(targets that ignore their input and run a fixed battery) is an anti-pattern — it +duplicates the unit-test role, adds heavyweight zkVM work to every corpus replay, +and risks silent corpus rot, all to satisfy a metric (Plane B) better served by +documenting the boundary. `lez-fuzzing` therefore keeps **no** input-independent +targets: the public/privacy execution targets (which duplicated existing `lee` +tests) and the three genesis/common/system targets (whose invariants were ported +to new unit tests — see the companion doc) were all removed. + +--- + +## Catalogue (Group 1 — structurally unreachable by fuzzing) + +The nine mutations reported as MISSED by the `mutants-protocol` run for which +fuzzing is structurally the wrong tool, with their true coverage. Verified by +applying each mutation to the `logos-execution-zone` working tree and running the +cited tests (`RISC0_DEV_MODE=1 cargo test -p lee --lib`). (Group 2 — the migrated +input-independent-target mutants — is summarised further down.) + +| # | Location | Mutation | Category | Covered by | +|---|----------|----------|----------|------------| +| 1 | `lee/state_machine/src/program.rs:21:51` | `*` → `/` (cycle limit `32`) | Valid-tx unit test | transfer-execution tests | +| 2 | `lee/state_machine/src/program.rs:21:51` | `*` → `+` (cycle limit `33 792`) | Valid-tx unit test | transfer-execution tests | +| 3 | `lee/state_machine/src/program.rs:21:58` | `*` → `/` (cycle limit `32 768`) | Valid-tx unit test | transfer-execution tests | +| 4 | `lee/state_machine/src/program.rs:21:58` | `*` → `+` (cycle limit `1 048 608`) | **Near-equivalent — genuine gap** | nothing (see below) | +| 5 | `lee/state_machine/src/validated_state_diff.rs:155:21` | `\|\|` → `&&` | Valid-tx unit test | transfer-execution tests | +| 6 | `lee/state_machine/src/validated_state_diff.rs:311:34` | `!=` → `==` | Misbehaving-program unit test | `public_changer_claimer_*` | +| 7 | `lee/state_machine/src/validated_state_diff.rs:314:20` | `==` → `!=` | Misbehaving-program unit test | `public_changer_claimer_*` + validity-window tests | +| 8 | `lee/state_machine/src/privacy_preserving_transaction/circuit.rs:88:32` | `>=` → `<` | Valid-PP-tx unit test | PP transition tests | +| 9 | `lee/state_machine/src/state.rs:335:16` | delete `!` | Valid-PP-tx unit test | PP transition tests | + +### Category A — Covered by `lee` unit tests, requires a valid *executing* transaction (1–3, 5, 8, 9) + +These fire only after a fully-valid transaction reaches real program execution. +A fuzzer's random bytes are rejected at the nonce/signature gate first, so the +corpus never reaches them; the `lee` crate pins each with a deterministic test. + +- **1–3 (public cycle limit, the catchable variants).** + `MAX_NUM_CYCLES_PUBLIC_EXECUTION = 1024 * 1024 * 32` (= 33 554 432). A real + `authenticated_transfer` execution consumes **between 33 792 and 1 048 608** + RISC-V cycles, so any mutation lowering the limit below that range aborts + execution with *"Session limit exceeded"*. + Covered by `state::tests::transition_from_authenticated_transfer_program_invocation_*` + (and the ~66 other public-execution tests that run a transfer). Verified: limit + `33 792` → 66 tests fail. + +- **5 (`||` → `&&` in `is_authorized`, + `validated_state_diff.rs:155`).** With `&&`, the transaction signer is no longer + treated as authorized, so a valid transfer fails with + `InvalidAccountAuthorization`. Covered by the same transfer-execution tests. + Verified: 3 of 7 `transition_from*` tests fail. + +- **8 (`>=` → `<` in `execute_and_prove`, + `circuit.rs:88`).** With `<`, the chained-call guard fires on the first + iteration (`0 < MAX`) and proving aborts immediately with + `MaxChainedCallsDepthExceeded`. Covered by + `state::tests::transition_from_privacy_preserving_transaction_{shielded,private,deshielded}`. + Verified: 3 PP tests fail. + +- **9 (delete `!` in `check_nullifiers_are_valid`, + `state.rs:335`).** Removing the `!` inverts the digest check so a *recognised* + commitment-set digest is rejected, breaking every valid privacy-preserving + transfer that spends a private input. Covered by the same PP transition tests. + Verified: 3 PP tests fail. + +### Category B — Covered by `lee` unit tests, requires a *misbehaving* program (6, 7) + +These guard against a program returning malformed output (modifying or claiming a +default account incorrectly). Only the test-only fixtures behind +`V03State::with_test_programs()` misbehave this way; they are never deployed, so no +fuzz input can reach this code. The `lee` crate exercises them directly. + +- **6 (`!=` → `==`, `validated_state_diff.rs:311`)** — the + "only inspect uninitialised accounts" filter. Verified: 1 test fails under the + full `lee` suite. +- **7 (`==` → `!=`, `validated_state_diff.rs:314`)** — the + "skip unmodified accounts" guard. Verified: 16 tests fail, including + `state::tests::public_changer_claimer_data_change_no_claim_fails` and + `public_changer_claimer_no_data_change_no_claim_succeeds`. + +> Note: an earlier analysis guessed 6 and 7 were *equivalent mutants*. They are +> not — they are caught by Plane A, just not reachable by Plane B. They appear +> "equivalent" only if you restrict yourself to the deployed `authenticated_transfer` +> program, which is exactly the restriction fuzzing operates under. + +### Category C — The single genuine gap: near-equivalent weak mutant (4) + +- **4 (`*` → `+` at `program.rs:21:58`, cycle limit `1 048 608`).** + Catching this would require a *single* public program execution that consumes + **more than 1 048 608 RISC-V cycles**. The `authenticated_transfer` instruction + uses fewer than that (it is caught only by limits ≤ 33 792 — see category A), and + no deployed program's single instruction reaches ~1M cycles. The difference + between the mutated limit (1.05M) and the real limit (33.5M) is therefore + **unobservable for any realistic workload**, making this a practically + equivalent / weak mutant. Verified: survives the full `lee` suite (211/211 pass). + + It is not worth chasing in either plane. If a future deployed program legitimately + performs a >1M-cycle public execution, a normal execution test for that program + would catch this mutation incidentally. + +--- + +## Group 2 — migrated input-independent targets + +These mutants used to be caught by Plane B via input-independent fuzz targets. +Those targets were removed and their invariants ported to LEZ unit tests, so the +mutants now survive Plane B by design. They are **not** structurally unreachable +like Group 1 — a fuzzer could "catch" them, but only by running a fixed scenario +that ignores its input, which is a unit test, not fuzzing. + +Each port below was verified to kill its mutant (apply the mutation → run the named +test → observe a failure). Where a mutant had **no** prior unit-test coverage, the +port *added* coverage rather than merely relocating it; those are marked **(new)**. + +**From `fuzz_common_invariants`:** + +| Mutant | New unit test | +|---|---| +| `HashType::as_ref` → `Vec::leak(Vec::new())` / `vec![0]` / `vec![1]` | `common::tests::as_ref_returns_exact_inner_bytes` (`common/src/lib.rs`) **(new)** | +| `BasicAuth` `FromStr` delete `!` in `.filter(\|p\| !p.is_empty())` | `common::config::tests::parse_empty_password_is_none` (+ `parse_preserves_non_empty_password`) **(new)** | +| `Program::elf` → empty / `vec![0]` / `vec![1]` | `program::tests::elf_returns_the_program_bytecode_constant` (was already caught incidentally) | +| `Proof::into_inner` / `from_inner` → `vec![]` / `vec![0]` / `vec![1]` | `…::circuit::tests::proof_inner_roundtrip` **(new)** | +| `Message::into_bytecode` → `vec![]` / `vec![0]` / `vec![1]` | `program_deployment_transaction::message::tests::bytecode_roundtrip` **(new)** | + +**From `fuzz_genesis_invariants`** (all in `lee/state_machine/src/state.rs`): + +| Mutant | New unit test | +|---|---| +| `system_faucet_account` → `Default` / delete `balance` / delete `program_owner` | `state::tests::genesis_system_accounts_have_expected_contents` **(new)** | +| `system_bridge_account` → `Default` / delete `program_owner` | `genesis_system_accounts_have_expected_contents` **(new)** | +| `commitment_set_digest` → `Default` | `state::tests::genesis_commitment_set_digest_differs_from_empty_state` **(new)** | +| `add_pinata_token_program` delete `program_owner` / `data` | `state::tests::add_pinata_token_program_sets_non_default_owner_and_data` **(new)** | +| `system_faucet_account_id` / `system_bridge_account_id` → `Default` | `genesis_system_accounts_have_expected_contents` + `system_account_ids_are_distinct_and_non_default` (was already caught) | + +**From `fuzz_system_account_protection`:** + +| Mutant | New unit test | +|---|---| +| `validate_doesnt_modify_account` `!=` → `==` (`common/src/transaction.rs`) | `common::transaction::tests::validate_on_state_rejects_modifying_a_system_account` **(new)** | +| `public_diff` → `HashMap::new()` (`lee/.../validated_state_diff.rs`) | `validated_state_diff::tests::public_diff_reflects_a_successful_transfer` (+ the `validate_on_state_rejects…` test) **(new)** | +| `system_*_account_id` non-default / distinct | `common::transaction::tests::system_account_ids_are_distinct_and_non_default` (was already caught) | + +--- + +## Re-verifying + +From `logos-execution-zone/` with the fuzzing repo checked out as a sibling: + +```bash +export RISC0_DEV_MODE=1 + +# Pick a mutation from a table above, apply it to the cited line, then run the +# owning crate's tests (Plane A). A real failure ⇒ unit tests cover it. +cargo test -p lee --lib # lee-owned mutants +cargo test -p common # common-owned mutants (Group 2) +git checkout -- # always revert +``` + +A mutation that makes `cargo test` fail is covered by Plane A and belongs in this +registry; a mutation that the corpus replay (`just mutants-protocol`) catches +belongs in the corpus instead. Across both groups, mutation #4 (the near-equivalent +cycle-limit weak mutant) is the only one caught by **neither** plane. + +> Tip: when reverting, prefer reverse-editing only the mutated line rather than +> `git checkout -- ` if you have uncommitted unit tests in the same file — +> a whole-file checkout would discard them too. From 0710dbfc2ba0b82a7a526d2fa0127434666fb3a1 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jun 2026 11:13:14 +0800 Subject: [PATCH 24/31] fix: update mutants workflow --- .github/workflows/mutants.yml | 9 ++++++--- corpus/libfuzz/fuzz_genesis_invariants/seed | Bin 1 -> 0 bytes 2 files changed, 6 insertions(+), 3 deletions(-) delete mode 100644 corpus/libfuzz/fuzz_genesis_invariants/seed diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index 1c8e70c9..44bdd251 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -107,7 +107,7 @@ jobs: fi # ── Plane B: mutate LEZ protocol code, oracle = corpus regression ───────── - # Each mutant: rebuild nssa/common + replay all 15 fuzz corpora (-runs=0). + # Each mutant: rebuild nssa/common + replay all 20 fuzz corpora (-runs=0). # Surviving mutants = protocol bugs the committed corpus has never caught. # Runs on schedule (weekly Monday) or manual workflow_dispatch only. mutants-protocol: @@ -149,8 +149,9 @@ jobs: - name: Make corpus-regression wrapper executable run: chmod +x scripts/mutants-corpus-test.sh - # Build all 15 fuzz targets once before the mutation loop so that each + # Build all 20 fuzz targets once before the mutation loop so that each # mutant only needs to rebuild the mutated crate, not the fuzz harness. + # Keep this list in sync with scripts/mutants-corpus-test.sh. - name: Pre-build fuzz targets run: | for target in \ @@ -161,7 +162,9 @@ jobs: fuzz_validate_execute_consistency fuzz_state_serialization \ fuzz_witness_set_verification fuzz_program_deployment_lifecycle \ fuzz_apply_state_diff_split_path fuzz_multi_block_state_sequence \ - fuzz_sequencer_vs_replayer; do + fuzz_sequencer_vs_replayer fuzz_merkle_tree \ + fuzz_transaction_properties fuzz_privacy_preserving_witness \ + fuzz_encoding_privacy_preserving fuzz_nullifier_set_roundtrip; do cargo fuzz build "${target}" done diff --git a/corpus/libfuzz/fuzz_genesis_invariants/seed b/corpus/libfuzz/fuzz_genesis_invariants/seed deleted file mode 100644 index f76dd238ade08917e6712764a16a22005a50573d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1 IcmZPo000310RR91 From cfc415d214555cf9a78c0a48636744ff66ec53a3 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jun 2026 11:47:43 +0800 Subject: [PATCH 25/31] fix: workflow files update - polish documentation --- .github/workflows/fuzz-afl.yml | 22 +++- .github/workflows/fuzz.yml | 17 ++- README.md | 168 ++++++++++++++--------------- current_vs_alternative_approach.md | 22 ++-- docs/fuzzing.md | 79 +++++++++----- docs/mutants-not-fuzzable.md | 14 +-- 6 files changed, 188 insertions(+), 134 deletions(-) diff --git a/.github/workflows/fuzz-afl.yml b/.github/workflows/fuzz-afl.yml index 60d31bd6..039ae915 100644 --- a/.github/workflows/fuzz-afl.yml +++ b/.github/workflows/fuzz-afl.yml @@ -41,6 +41,11 @@ jobs: - fuzz_transaction_decoding - fuzz_validate_execute_consistency - fuzz_witness_set_verification + - fuzz_merkle_tree + - fuzz_transaction_properties + - fuzz_privacy_preserving_witness + - fuzz_encoding_privacy_preserving + - fuzz_nullifier_set_roundtrip steps: - name: Checkout repository @@ -239,7 +244,7 @@ jobs: if-no-files-found: ignore # ──────────────────────────────────────────────────────────────────────────── - # afl-coverage-aggregate — single HTML report merging all 15 targets + # afl-coverage-aggregate — single HTML report merging all 20 targets # ──────────────────────────────────────────────────────────────────────────── afl-coverage-aggregate: name: "AFL++ coverage — aggregated" @@ -302,6 +307,11 @@ jobs: fuzz_transaction_decoding fuzz_validate_execute_consistency fuzz_witness_set_verification + fuzz_merkle_tree + fuzz_transaction_properties + fuzz_privacy_preserving_witness + fuzz_encoding_privacy_preserving + fuzz_nullifier_set_roundtrip ) for TARGET in "${TARGETS[@]}"; do cargo build \ @@ -330,6 +340,11 @@ jobs: fuzz_transaction_decoding fuzz_validate_execute_consistency fuzz_witness_set_verification + fuzz_merkle_tree + fuzz_transaction_properties + fuzz_privacy_preserving_witness + fuzz_encoding_privacy_preserving + fuzz_nullifier_set_roundtrip ) PROFRAW_DIR="coverage/afl/aggregated/profraw" mkdir -p "$PROFRAW_DIR" @@ -409,6 +424,11 @@ jobs: fuzz_transaction_decoding fuzz_validate_execute_consistency fuzz_witness_set_verification + fuzz_merkle_tree + fuzz_transaction_properties + fuzz_privacy_preserving_witness + fuzz_encoding_privacy_preserving + fuzz_nullifier_set_roundtrip ) # llvm-cov show: first binary is a positional arg; the rest use --object first=1 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 4deb722d..4970f7b0 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -35,6 +35,11 @@ jobs: - fuzz_apply_state_diff_split_path - fuzz_multi_block_state_sequence - fuzz_sequencer_vs_replayer + - fuzz_merkle_tree + - fuzz_transaction_properties + - fuzz_privacy_preserving_witness + - fuzz_encoding_privacy_preserving + - fuzz_nullifier_set_roundtrip steps: - uses: actions/checkout@v4 @@ -218,6 +223,11 @@ jobs: - fuzz_apply_state_diff_split_path - fuzz_multi_block_state_sequence - fuzz_sequencer_vs_replayer + - fuzz_merkle_tree + - fuzz_transaction_properties + - fuzz_privacy_preserving_witness + - fuzz_encoding_privacy_preserving + - fuzz_nullifier_set_roundtrip steps: - uses: actions/checkout@v4 - name: Checkout logos-execution-zone @@ -285,7 +295,12 @@ jobs: fuzz_program_deployment_lifecycle \ fuzz_apply_state_diff_split_path \ fuzz_multi_block_state_sequence \ - fuzz_sequencer_vs_replayer; do + fuzz_sequencer_vs_replayer \ + fuzz_merkle_tree \ + fuzz_transaction_properties \ + fuzz_privacy_preserving_witness \ + fuzz_encoding_privacy_preserving \ + fuzz_nullifier_set_roundtrip; do echo "=== $target ===" | tee -a perf_baseline.txt cargo fuzz run "$target" -- -max_total_time=30 2>&1 \ | grep -E "exec/s|execs_per_sec" | tail -1 | tee -a perf_baseline.txt diff --git a/README.md b/README.md index bc5b4a92..6675d3e2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@ -# Lez-fuzzing +
-Coverage-guided fuzzing and adversarial testing infrastructure for the -**Logos Execution Zone (LEZ)** protocol. +# 🦀 Lez-fuzzing + +**Coverage-guided fuzzing & adversarial testing infrastructure for the +[Logos Execution Zone (LEZ)](https://github.com/) protocol.** + +[![Rust](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-20%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) + +
--- -## Repository Layout +## 📂 Repository Layout ``` lez-fuzzing/ @@ -22,28 +31,9 @@ lez-fuzzing/ │ └── generators.rs # Arbitrary / proptest strategies ├── fuzz/ # cargo-fuzz crate (own [workspace] sentinel) │ ├── Cargo.toml -│ ├── fuzz_targets/ -│ │ ├── _template.rs # Template for just new-target -│ │ ├── fuzz_transaction_decoding.rs -│ │ ├── fuzz_stateless_verification.rs -│ │ ├── fuzz_state_transition.rs -│ │ ├── fuzz_block_verification.rs -│ │ ├── fuzz_encoding_roundtrip.rs -│ │ ├── fuzz_signature_verification.rs -│ │ ├── fuzz_replay_prevention.rs -│ │ ├── fuzz_state_diff_computation.rs -│ │ ├── fuzz_validate_execute_consistency.rs -│ │ ├── fuzz_state_serialization.rs -│ │ ├── fuzz_witness_set_verification.rs -│ │ ├── fuzz_program_deployment_lifecycle.rs -│ │ ├── fuzz_apply_state_diff_split_path.rs -│ │ ├── fuzz_multi_block_state_sequence.rs -│ │ ├── fuzz_sequencer_vs_replayer.rs -│ │ ├── fuzz_merkle_tree.rs -│ │ ├── fuzz_transaction_properties.rs -│ │ ├── fuzz_privacy_preserving_witness.rs -│ │ ├── fuzz_encoding_privacy_preserving.rs -│ │ └── fuzz_nullifier_set_roundtrip.rs # 20 targets total — see table below +│ ├── fuzz_targets/ # 20 targets total — see table below +│ │ ├── _template.rs # Template for `just new-target` +│ │ └── fuzz_*.rs │ └── corpus/ # Curated seed inputs (one dir per target) ├── .github/ │ └── workflows/ @@ -54,11 +44,12 @@ lez-fuzzing/ ├── scripts/ │ └── add_fuzz_target.py # Automates new-target scaffolding (called by just new-target) └── docs/ - └── fuzzing.md # Full developer guide + ├── fuzzing.md # Full developer guide + └── mutants-not-fuzzable.md # Policy + mutant→test mapping ``` The LEZ codebase is consumed as a **sibling directory** — clone -`logos-execution-zone` next to this repository: +`logos-execution-zone` next to this repository so the `../` path deps resolve: ``` parent/ @@ -68,25 +59,25 @@ parent/ --- -## Quick Start +## 🚀 Quick Start -### Prerequisites +### 1. Prerequisites ```bash rustup install nightly rustup component add llvm-tools-preview --toolchain nightly cargo install cargo-fuzz -# Optional but recommended: -cargo install just +cargo install just # optional but recommended ``` +> [!NOTE] > **Why nightly?** `cargo-fuzz` passes `-Zsanitizer=address` and > `-Zinstrument-coverage` (unstable flags) to `rustc`, and depends on the -> `llvm-tools-preview` nightly component for coverage reporting. The -> `rust-toolchain.toml` pins the whole repository to nightly so you never +> `llvm-tools-preview` nightly component for coverage reporting. +> `rust-toolchain.toml` pins the whole repository to nightly, so you never > need an explicit `+nightly` flag. -### Setup +### 2. Setup ```bash # Clone both repositories side by side @@ -95,7 +86,7 @@ git clone lez-fuzzing cd lez-fuzzing ``` -### Run the fuzz targets +### 3. Run the fuzz targets ```bash # All targets for 30 s each (RISC0_DEV_MODE=1 is set automatically) @@ -114,38 +105,42 @@ just fuzz-regression just fuzz-props ``` +> [!IMPORTANT] > **ZK-proof cost:** `RISC0_DEV_MODE=1` is exported at the top of the > `Justfile` and must be set in every fuzz run to stub out ZK proof -> generation. Without it each execution takes seconds instead of -> microseconds. +> generation. Without it, each execution takes **seconds** instead of +> **microseconds**. --- -## Fuzz Targets +## 🎯 Fuzz Targets -| Target | Protocol layer | Entry point | -|--------|---------------|-------------| -| `fuzz_transaction_decoding` | Borsh decoding of all tx/block types (`LeeTransaction`, `Block`, `HashableBlockData`) with roundtrip re-encoding | `fuzz/fuzz_targets/fuzz_transaction_decoding.rs` | -| `fuzz_stateless_verification` | `transaction_stateless_check()` no-panic + idempotency | `fuzz/fuzz_targets/fuzz_stateless_verification.rs` | -| `fuzz_state_transition` | `V03State` transition: StateIsolationOnFailure + BalanceConservation + ReplayRejection invariants across up to 8 txs with fuzz-driven state | `fuzz/fuzz_targets/fuzz_state_transition.rs` | -| `fuzz_block_verification` | Block hash integrity: HashRoundTrip · HashPreimage completeness (block_id/prev_hash/timestamp) · TxOrderCommitment | `fuzz/fuzz_targets/fuzz_block_verification.rs` | -| `fuzz_encoding_roundtrip` | Borsh encode→decode→encode round-trip identity + canonical encoding for `PublicTransaction` and `ProgramDeploymentTransaction` | `fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs` | -| `fuzz_signature_verification` | Signature correctness (sign→verify), no-panic on random bytes, cross-key soundness | `fuzz/fuzz_targets/fuzz_signature_verification.rs` | -| `fuzz_replay_prevention` | Transaction nonce replay rejection with fuzz-driven initial state | `fuzz/fuzz_targets/fuzz_replay_prevention.rs` | -| `fuzz_state_diff_computation` | `ValidatedStateDiff` forward containment + reverse completeness (bidirectional isolation check) | `fuzz/fuzz_targets/fuzz_state_diff_computation.rs` | -| `fuzz_validate_execute_consistency` | `validate_on_state` / `execute_check_on_state` agreement + diff accuracy + BalanceConservation | `fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs` | -| `fuzz_state_serialization` | `V03State` Borsh decode no-panic + StateSerializationRoundtrip idempotency + NullifierDeduplication (`NullifierSet` hand-written impl) | `fuzz/fuzz_targets/fuzz_state_serialization.rs` | -| `fuzz_witness_set_verification` | `WitnessSet::is_valid_for` no-panic + CorrectVerification (sign→verify) + MessageIsolation (witness set for msg A rejected on msg B) | `fuzz/fuzz_targets/fuzz_witness_set_verification.rs` | -| `fuzz_program_deployment_lifecycle` | `V03State::transition_from_program_deployment_transaction` no-panic + BalanceIsolation (deployment must not move tokens) + StateIsolationOnFailure | `fuzz/fuzz_targets/fuzz_program_deployment_lifecycle.rs` | -| `fuzz_apply_state_diff_split_path` | SplitPathEquivalence: `validate_on_state + apply_state_diff` == `execute_check_on_state` for all known accounts (balance, nonce, data, program_owner); NonceIncrementCorrectness | `fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs` | -| `fuzz_multi_block_state_sequence` | LongRangeBalanceConservation across up to 16 blocks + FailedTxNonceStability (nonce must not change on rejection) + PerBlockReplayRejection | `fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs` | -| `fuzz_sequencer_vs_replayer` | Differential: sequencer path (`validate_on_state` → `apply_state_diff`) vs replayer path (`execute_check_on_state`) — SequencerReplayerEquivalence + ReplayerAcceptsAllSequencerTxs + ClockConsistency | `fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs` | -| `fuzz_merkle_tree` | Commitment Merkle tree via the commitment set: ProofSome · ProofValid (leaf + auth path recomputes the root) · NonMembershipNone · IndicesSequential | `fuzz/fuzz_targets/fuzz_merkle_tree.rs` | -| `fuzz_transaction_properties` | Transaction property invariants: HashDeterministic/HashNonDefault, SignerIds derived from witness keys & non-empty, AffectedAccountsContainSigners, PublicDiffNonEmptyOnSuccess | `fuzz/fuzz_targets/fuzz_transaction_properties.rs` | -| `fuzz_privacy_preserving_witness` | `privacy_preserving_transaction::WitnessSet`: CorrectVerification (witness for msg A passes `signatures_are_valid_for(A)`) + MessageIsolation + SignerIdsMatchWitnessKeys | `fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs` | -| `fuzz_encoding_privacy_preserving` | Privacy-preserving encoding: MessageEncodingRoundtrip + TxEncodingDeterministic/NonEmpty | `fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs` | -| `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: NullifierSetRoundtrip (decode→encode identity for the hand-written impl) | `fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs` | +| # | Target | Protocol layer | +|---|--------|----------------| +| 1 | `fuzz_transaction_decoding` | Borsh decoding of all tx/block types (`LeeTransaction`, `Block`, `HashableBlockData`) with roundtrip re-encoding | +| 2 | `fuzz_stateless_verification` | `transaction_stateless_check()` no-panic + idempotency | +| 3 | `fuzz_state_transition` | `V03State` transition: StateIsolationOnFailure + BalanceConservation + ReplayRejection invariants across up to 8 txs with fuzz-driven state | +| 4 | `fuzz_block_verification` | Block hash integrity: HashRoundTrip · HashPreimage completeness (block_id/prev_hash/timestamp) · TxOrderCommitment | +| 5 | `fuzz_encoding_roundtrip` | Borsh encode→decode→encode round-trip identity + canonical encoding for `PublicTransaction` and `ProgramDeploymentTransaction` | +| 6 | `fuzz_signature_verification` | Signature correctness (sign→verify), no-panic on random bytes, cross-key soundness | +| 7 | `fuzz_replay_prevention` | Transaction nonce replay rejection with fuzz-driven initial state | +| 8 | `fuzz_state_diff_computation` | `ValidatedStateDiff` forward containment + reverse completeness (bidirectional isolation check) | +| 9 | `fuzz_validate_execute_consistency` | `validate_on_state` / `execute_check_on_state` agreement + diff accuracy + BalanceConservation | +| 10 | `fuzz_state_serialization` | `V03State` Borsh decode no-panic + StateSerializationRoundtrip idempotency + NullifierDeduplication (`NullifierSet` hand-written impl) | +| 11 | `fuzz_witness_set_verification` | `WitnessSet::is_valid_for` no-panic + CorrectVerification (sign→verify) + MessageIsolation (witness set for msg A rejected on msg B) | +| 12 | `fuzz_program_deployment_lifecycle` | `V03State::transition_from_program_deployment_transaction` no-panic + BalanceIsolation (deployment must not move tokens) + StateIsolationOnFailure | +| 13 | `fuzz_apply_state_diff_split_path` | SplitPathEquivalence: `validate_on_state + apply_state_diff` == `execute_check_on_state` for all known accounts (balance, nonce, data, program_owner); NonceIncrementCorrectness | +| 14 | `fuzz_multi_block_state_sequence` | LongRangeBalanceConservation across up to 16 blocks + FailedTxNonceStability (nonce must not change on rejection) + PerBlockReplayRejection | +| 15 | `fuzz_sequencer_vs_replayer` | Differential: sequencer path (`validate_on_state` → `apply_state_diff`) vs replayer path (`execute_check_on_state`) — SequencerReplayerEquivalence + ReplayerAcceptsAllSequencerTxs + ClockConsistency | +| 16 | `fuzz_merkle_tree` | Commitment Merkle tree via the commitment set: ProofSome · ProofValid (leaf + auth path recomputes the root) · NonMembershipNone · IndicesSequential | +| 17 | `fuzz_transaction_properties` | Transaction property invariants: HashDeterministic/HashNonDefault, SignerIds derived from witness keys & non-empty, AffectedAccountsContainSigners, PublicDiffNonEmptyOnSuccess | +| 18 | `fuzz_privacy_preserving_witness` | `privacy_preserving_transaction::WitnessSet`: CorrectVerification (witness for msg A passes `signatures_are_valid_for(A)`) + MessageIsolation + SignerIdsMatchWitnessKeys | +| 19 | `fuzz_encoding_privacy_preserving` | Privacy-preserving encoding: MessageEncodingRoundtrip + TxEncodingDeterministic/NonEmpty | +| 20 | `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: NullifierSetRoundtrip (decode→encode identity for the hand-written impl) | +Each target lives at `fuzz/fuzz_targets/.rs`. + +> [!NOTE] > **Input-independent checks are not fuzz targets here.** Deterministic invariants > that ignore their input (e.g. genesis-account contents, getter/round-trip > identities, the system-account-modification guard) belong in `logos-execution-zone` @@ -155,7 +150,7 @@ just fuzz-props --- -## Corpus Management +## 🧬 Corpus Management ```bash # Minimise all corpora (removes dominated inputs, keeps coverage-equivalent set) @@ -167,52 +162,52 @@ just corpus-cmin-target fuzz_state_transition --- -## Crash / Failure Workflow +## 💥 Crash / Failure Workflow ```bash -# Minimise a crash artifact +# 1. Minimise a crash artifact just fuzz-tmin fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-abc123 -# Print the bytes as a Rust literal (for a regression #[test]) +# 2. Print the bytes as a Rust literal (for a regression #[test]) cargo fuzz fmt fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-abc123 -# Promote the minimised input to the corpus so CI catches regressions +# 3. Promote the minimised input to the corpus so CI catches regressions cp fuzz/artifacts/fuzz_state_transition/crash-abc123-minimised \ fuzz/corpus/fuzz_state_transition/regression_001 ``` --- -## Adding a New Target +## ➕ Adding a New Target ```bash # Scaffold everything automatically (corpus dir, .rs file, Cargo.toml entry, CI matrix entry) just new-target my_feature # creates fuzz_my_feature ``` -`just new-target` calls [`scripts/add_fuzz_target.py`](scripts/add_fuzz_target.py) which +`just new-target` calls [`scripts/add_fuzz_target.py`](scripts/add_fuzz_target.py), which appends the `[[bin]]` entry to [`fuzz/Cargo.toml`](fuzz/Cargo.toml) and inserts the target into every strategy matrix in [`.github/workflows/fuzz.yml`](.github/workflows/fuzz.yml). --- -## Housekeeping +## 🧹 Housekeeping -```bash -just clean # Remove Cargo build artefacts (target/ and fuzz/target/) -just clean-artifacts # Remove fuzz/artifacts/ (crash/timeout inputs) -just clean-coverage # Remove fuzz/coverage/ (LLVM coverage reports) -just clean-all # All of the above -``` +| Command | Removes | +|---------|---------| +| `just clean` | Cargo build artefacts (`target/` and `fuzz/target/`) | +| `just clean-artifacts` | `fuzz/artifacts/` (crash/timeout inputs) | +| `just clean-coverage` | `fuzz/coverage/` (LLVM coverage reports) | +| `just clean-all` | All of the above | --- -## CI +## ⚙️ CI GitHub Actions runs these workflows on every push/PR and nightly: | Workflow | What it does | -|----------|-------------| +|----------|--------------| | `fuzz.yml` — `smoke-fuzz` (matrix) | Builds + runs each libFuzzer target for 60 s | | `fuzz.yml` — `regression` (matrix) | Replays the saved corpus (`-runs=0`) | | `fuzz.yml` — `proptest` | `cargo test -p fuzz_props --release` | @@ -221,22 +216,23 @@ GitHub Actions runs these workflows on every push/PR and nightly: | `mutants.yml` | Mutation testing (`cargo-mutants`) | | `lint.yml` | Formatting + Clippy | -> **Note:** The `fuzz.yml` matrix currently lists 15 of the 20 libFuzzer targets. -> Still missing: `fuzz_merkle_tree`, `fuzz_transaction_properties`, -> `fuzz_privacy_preserving_witness`, `fuzz_encoding_privacy_preserving`, and -> `fuzz_nullifier_set_roundtrip` — add them to `.github/workflows/fuzz.yml`. See -> [`docs/fuzzing.md`](docs/fuzzing.md) for the manual fallback instructions. +> [!NOTE] +> All **20** libFuzzer targets are wired into every `fuzz.yml` matrix +> (smoke-fuzz · regression · perf-baseline), the `fuzz-afl.yml` AFL++ lane, and +> the `mutants.yml` corpus-replay job. New targets are added automatically by +> `just new-target`; see [`docs/fuzzing.md`](docs/fuzzing.md) for the manual +> fallback instructions. --- -## Documentation +## 📖 Documentation -Full developer guide — how to add new targets, interpret crashes, update -the LEZ sibling clone, and tune performance — is in +The full developer guide — how to add new targets, interpret crashes, update +the LEZ sibling clone, and tune performance — lives in [`docs/fuzzing.md`](docs/fuzzing.md). --- -## License +## 📜 License Licensed under the [MIT License](LICENSE-MIT). diff --git a/current_vs_alternative_approach.md b/current_vs_alternative_approach.md index ba97b255..e20982c8 100644 --- a/current_vs_alternative_approach.md +++ b/current_vs_alternative_approach.md @@ -1,6 +1,6 @@ # Alternative Approaches vs. Current Implementation -## What the Current Project Does +## 🧩 What the Current Project Does The `lez-fuzzing` repository is a **coverage-guided, structured mutation fuzzing system** built on **cargo-fuzz / libFuzzer**, operating as a standalone companion to the Logos Execution Zone (LEZ) codebase. Its key design pillars: @@ -17,7 +17,7 @@ The `lez-fuzzing` repository is a **coverage-guided, structured mutation fuzzing --- -## Alternative Approaches +## 🔬 Alternative Approaches ### 1. AFL++ (American Fuzzy Lop++) @@ -127,7 +127,7 @@ fuzzing replacement. --- -## Summary Comparison Matrix +## 📊 Summary Comparison Matrix | Approach | Bug-finding depth | CI cost | Impl. cost | Complements current? | Recommended action | |---|---|---|---|---|---| @@ -141,7 +141,7 @@ fuzzing replacement. --- -## Decision-maker Recommendations +## 🧭 Decision-maker Recommendations **Already done** (was previously recommended here): @@ -150,16 +150,16 @@ fuzzing replacement. with the Plane A / Plane B framework documented in [`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md). - ✅ **Differential testing** — `fuzz_sequencer_vs_replayer`. +- ✅ **Complete `fuzz.yml` CI matrix** — all **20** libFuzzer targets now run in + every `fuzz.yml` matrix (smoke-fuzz · regression · perf-baseline) and the + `fuzz-afl.yml` AFL++ lane, including `fuzz_merkle_tree`, + `fuzz_transaction_properties`, `fuzz_privacy_preserving_witness`, + `fuzz_encoding_privacy_preserving`, and `fuzz_nullifier_set_roundtrip`. **Remaining higher-ROI next steps, in priority order:** -1. **Finish the `fuzz.yml` CI matrix** — it lists 15 of the 20 libFuzzer targets; - add `fuzz_merkle_tree`, `fuzz_transaction_properties`, - `fuzz_privacy_preserving_witness`, `fuzz_encoding_privacy_preserving`, and - `fuzz_nullifier_set_roundtrip`. - -2. **Honggfuzz on Linux CI only** — hardware-counter coverage finds different paths; +1. **Honggfuzz on Linux CI only** — hardware-counter coverage finds different paths; gated to Linux since Apple Silicon has no HW counters. -3. **Formal verification of core invariants** (balance conservation, replay +2. **Formal verification of core invariants** (balance conservation, replay prevention) — a long-term supplement, not a replacement. \ No newline at end of file diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 5c3579ef..a714da0e 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -1,4 +1,10 @@ -# Fuzzing Guide +
+ +# 🔬 Fuzzing Guide + +**The full developer guide to running, extending, and triaging the LEZ fuzzing infrastructure.** + +
This document covers how to run fuzz targets, add new targets, minimise failures, and convert findings into regression tests. @@ -9,7 +15,7 @@ directory that must be cloned separately). --- -## Architecture +## 🏗️ Architecture The fuzz workspace (`fuzz/`) is a single Cargo workspace that covers **both** fuzzing engines via Cargo features. No separate Cargo manifest is needed. @@ -38,7 +44,7 @@ The `cfg` attributes in the macro expansion resolve against the **calling crate' --- -## Prerequisites +## 🧰 Prerequisites ```bash # libFuzzer lane @@ -61,7 +67,7 @@ cargo install cargo-afl --- -## Repository Setup +## 📁 Repository Setup `lez-fuzzing` is a **standalone repository** — it does **not** use git submodules. It expects the LEZ codebase to be cloned at `../logos-execution-zone` relative to itself. @@ -79,7 +85,7 @@ git clone lez-fuzzing --- -## How to Run +## ▶️ How to Run All fuzz targets must be run with `RISC0_DEV_MODE=1` to disable expensive ZK proof generation. The `just` recipes handle this automatically. @@ -99,7 +105,7 @@ just fuzz-regression --- -## Available Fuzz Targets +## 🎯 Available Fuzz Targets | Target | What it fuzzes | Entry point | |--------|---------------|-------------| @@ -118,10 +124,15 @@ just fuzz-regression | `fuzz_apply_state_diff_split_path` | **SplitPathEquivalence**: for every known account, `validate_on_state` + `apply_state_diff` must produce exactly the same balance, nonce, data, and program_owner as `execute_check_on_state`; **NonceIncrementCorrectness**: nonce after the split path equals nonce after the direct path for all signer accounts (catches bugs in the two-step `apply_state_diff` nonce-increment logic) | `fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs` | | `fuzz_multi_block_state_sequence` | **LongRangeBalanceConservation**: total genesis-account balance identical before and after all N (≤ 16) blocks; **FailedTxNonceStability**: every genesis-account nonce unchanged after a rejected transaction; **PerBlockReplayRejection**: every transaction accepted in block B is rejected in block B+1 (cumulative nonce-interaction coverage) | `fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs` | | `fuzz_sequencer_vs_replayer` | **SequencerReplayerEquivalence**: for every known account (genesis ∪ diff-declared), the sequencer path (`validate_on_state` → `apply_state_diff`) and the replayer path (`execute_check_on_state`) must produce identical balance, nonce, data, and program_owner after applying a full block of up to 8 transactions plus the mandatory clock invocation; **ReplayerAcceptsAllSequencerTxs**: every transaction accepted by `validate_on_state` must also be accepted by `execute_check_on_state`; **ClockConsistency**: the mandatory clock invocation must succeed on both paths and leave both states identical | `fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs` | +| `fuzz_merkle_tree` | Commitment Merkle tree via the commitment set: **ProofSome**, **ProofValid** (leaf + auth path recomputes the root), **NonMembershipNone**, **IndicesSequential** | `fuzz/fuzz_targets/fuzz_merkle_tree.rs` | +| `fuzz_transaction_properties` | Transaction property invariants: **HashDeterministic** / **HashNonDefault**, **SignerIds** derived from witness keys & non-empty, **AffectedAccountsContainSigners**, **PublicDiffNonEmptyOnSuccess** | `fuzz/fuzz_targets/fuzz_transaction_properties.rs` | +| `fuzz_privacy_preserving_witness` | `privacy_preserving_transaction::WitnessSet`: **CorrectVerification** (witness for message A passes `signatures_are_valid_for(A)`), **MessageIsolation**, **SignerIdsMatchWitnessKeys** | `fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs` | +| `fuzz_encoding_privacy_preserving` | Privacy-preserving encoding: **MessageEncodingRoundtrip**, **TxEncodingDeterministic** / **NonEmpty** | `fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs` | +| `fuzz_nullifier_set_roundtrip` | `NullifierSet` Borsh serialisation: **NullifierSetRoundtrip** (decode→encode identity for the hand-written impl) | `fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs` | --- -## How to Add a New Fuzz Target +## ➕ How to Add a New Fuzz Target ### Step 1 — Scaffold with `just new-target` @@ -159,6 +170,7 @@ which: - Inserts the target name into every strategy matrix and the perf-baseline shell loop in [`.github/workflows/fuzz.yml`](../.github/workflows/fuzz.yml). +> [!TIP] > **Manual fallback:** if you create a target without `just new-target`, add the > entry yourself: > @@ -197,12 +209,13 @@ cd fuzz && cargo afl build \ --- -## AFL++ Parallel Fuzzing Lane +## 🔀 AFL++ Parallel Fuzzing Lane ### Prerequisites Install AFL++ natively on your machine. +> [!NOTE] > **Note on Linux package versions**: The `afl++` package in Debian stable (Bookworm) > and Ubuntu LTS is several major versions behind the current AFL++ 4.x series and may > be incompatible with `cargo-afl`. **Build from source** for a current version. @@ -222,6 +235,7 @@ cd .. cargo install cargo-afl ``` +> [!IMPORTANT] > **macOS: run `afl-system-config` once before fuzzing** — AFL++ uses System V shared > memory (`shmget`) to pass coverage bitmaps between the fuzzer and the target. macOS > ships with very small defaults (`kern.sysv.shmmax = 4 MB`, `kern.sysv.shmmni = 32`) @@ -245,6 +259,7 @@ cargo install cargo-afl > each restart. The `just fuzz-afl` and `just fuzz-afl-parallel` recipes **do not** > call this automatically because it requires `sudo`. +> [!IMPORTANT] > **macOS: crash reporter must be disabled** — AFL++ detects the macOS `ReportCrash` > daemon and aborts if it is active (it delays crash notifications and causes AFL++ to > mis-classify crashes as timeouts). The `just fuzz-afl` and `just fuzz-afl-parallel` @@ -361,24 +376,24 @@ The nightly AFL++ CI workflow has two jobs: | Job | Triggers | Matrix | |-----|----------|--------| -| `afl-smoke` | nightly + `workflow_dispatch` | 7 priority targets, 120 s each | -| `afl-coverage` | nightly, `needs: afl-smoke` | 3 key targets; LLVM HTML report | +| `afl-smoke` | nightly + `workflow_dispatch` | all 20 targets, 60 s each | +| `afl-coverage-aggregate` | nightly, `needs: afl-smoke` | all 20 targets merged into one LLVM HTML report | -The smoke job: -1. Builds the target with `cargo afl build --no-default-features --features fuzzer-afl` -2. Runs `afl-fuzz` for 120 s in `aflplusplus/aflplusplus:v4.40c` container -3. Syncs new queue entries into `fuzz/corpus//` and opens a corpus PR -4. Uploads crashes/hangs as a workflow artifact +The smoke job (one matrix leg per target, on `ubuntu-latest`): +1. Builds AFL++ from source, then builds the target with `cargo afl build --no-default-features --features fuzzer-afl` +2. Runs `afl-fuzz` for 60 s (`timeout 60`) +3. Reports edge-bitmap coverage to the job step summary +4. Uploads the queue/crashes/hangs as a workflow artifact -The coverage job: -1. Downloads the smoke findings -2. Rebuilds with `RUSTFLAGS="-C instrument-coverage"` -3. Runs all corpus + queue inputs through the binary -4. Merges `.profraw` → `.profdata` → HTML report via `llvm-cov show` +The coverage-aggregate job: +1. Downloads every smoke leg's findings +2. Rebuilds all 20 targets with `RUSTFLAGS="-C instrument-coverage"` +3. Runs all checked-in corpus + AFL queue inputs through each binary +4. Merges every `.profraw` → one `.profdata` → a single combined HTML report via `llvm-cov show` --- -## Updating the LEZ Dependency +## 🔄 Updating the LEZ Dependency `lez-fuzzing` reads LEZ source directly from `../logos-execution-zone`. To pick up LEZ changes, simply update that repo: @@ -401,7 +416,7 @@ just update-lez --- -## Minimising & Reproducing Failures +## 🐛 Minimising & Reproducing Failures When `cargo fuzz` finds a crash it writes an artifact to `fuzz/artifacts/fuzz_/crash-`. @@ -440,7 +455,7 @@ Open a PR. The `regression` CI job will permanently block re-introduction of thi --- -## Coverage Reports +## 📊 Coverage Reports ### Step 1 — libFuzzer coverage (via `cargo fuzz coverage`) @@ -476,7 +491,7 @@ automates steps 2–5 and uploads the report as a workflow artifact. --- -## Invariant Framework +## 🛡️ Invariant Framework Shared invariants live in `fuzz_props/src/invariants.rs`. There are two layers: @@ -538,7 +553,7 @@ To add a new invariant: --- -## Input Generators +## 🎲 Input Generators The `fuzz_props` crate provides two layers of input generation: @@ -579,7 +594,7 @@ fuzz target parameters for zero-boilerplate structured fuzzing. --- -## Performance Baseline +## ⚡ Performance Baseline Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`: @@ -600,7 +615,13 @@ Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`: | `fuzz_apply_state_diff_split_path` | ~5 000 exec/sec *(estimate)* | | `fuzz_multi_block_state_sequence` | ~1 000 exec/sec *(estimate)* | | `fuzz_sequencer_vs_replayer` | ~2 000 exec/sec *(estimate)* | +| `fuzz_merkle_tree` | ~20 000 exec/sec *(estimate)* | +| `fuzz_transaction_properties` | ~15 000 exec/sec *(estimate)* | +| `fuzz_privacy_preserving_witness` | ~15 000 exec/sec *(estimate)* | +| `fuzz_encoding_privacy_preserving` | ~50 000 exec/sec *(estimate)* | +| `fuzz_nullifier_set_roundtrip` | ~100 000 exec/sec *(estimate)* | +> [!NOTE] > Throughput figures for the five new targets are rough estimates; run `just perf-baseline` > locally or check the `perf-baseline` CI artifact for up-to-date measurements. @@ -617,7 +638,7 @@ just fuzz-afl-parallel fuzz_state_transition $(nproc) 3600 --- -## ZK-Proof Cost Warning +## ⚠️ ZK-Proof Cost Warning `PrivacyPreservingTransaction` uses `risc0-zkvm` (seconds per proof). All fuzz targets **must** set `RISC0_DEV_MODE=1` in the environment and the `just` @@ -632,7 +653,7 @@ flag stubs out ZK proof generation and replaces it with a fast mock implementati --- -## Mutation testing — the two planes +## 🧬 Mutation testing — the two planes Mutation testing here runs in two distinct planes, answering two different questions: @@ -666,7 +687,7 @@ from `data`; if a check doesn't depend on the input, write it as a unit test in --- -## Known Limitations & Future Work +## 🚧 Known Limitations & Future Work | Item | Notes | |------|-------| diff --git a/docs/mutants-not-fuzzable.md b/docs/mutants-not-fuzzable.md index 0efdf031..e703da97 100644 --- a/docs/mutants-not-fuzzable.md +++ b/docs/mutants-not-fuzzable.md @@ -35,7 +35,7 @@ mutant on **neither** list warrants a new corpus input. --- -## Why fuzzing is the wrong tool for these +## 🧭 Why fuzzing is the wrong tool for these Fuzzing earns its keep by exploring a large, *unknown* input space to find inputs a human wouldn't think of — malformed transactions, adversarial byte sequences, @@ -74,7 +74,7 @@ to new unit tests — see the companion doc) were all removed. --- -## Catalogue (Group 1 — structurally unreachable by fuzzing) +## 📋 Catalogue (Group 1 — structurally unreachable by fuzzing) The nine mutations reported as MISSED by the `mutants-protocol` run for which fuzzing is structurally the wrong tool, with their true coverage. Verified by @@ -143,7 +143,8 @@ fuzz input can reach this code. The `lee` crate exercises them directly. `state::tests::public_changer_claimer_data_change_no_claim_fails` and `public_changer_claimer_no_data_change_no_claim_succeeds`. -> Note: an earlier analysis guessed 6 and 7 were *equivalent mutants*. They are +> [!NOTE] +> an earlier analysis guessed 6 and 7 were *equivalent mutants*. They are > not — they are caught by Plane A, just not reachable by Plane B. They appear > "equivalent" only if you restrict yourself to the deployed `authenticated_transfer` > program, which is exactly the restriction fuzzing operates under. @@ -165,7 +166,7 @@ fuzz input can reach this code. The `lee` crate exercises them directly. --- -## Group 2 — migrated input-independent targets +## 🔁 Group 2 — migrated input-independent targets These mutants used to be caught by Plane B via input-independent fuzz targets. Those targets were removed and their invariants ported to LEZ unit tests, so the @@ -207,7 +208,7 @@ port *added* coverage rather than merely relocating it; those are marked **(new) --- -## Re-verifying +## ✅ Re-verifying From `logos-execution-zone/` with the fuzzing repo checked out as a sibling: @@ -226,6 +227,7 @@ registry; a mutation that the corpus replay (`just mutants-protocol`) catches belongs in the corpus instead. Across both groups, mutation #4 (the near-equivalent cycle-limit weak mutant) is the only one caught by **neither** plane. -> Tip: when reverting, prefer reverse-editing only the mutated line rather than +> [!TIP] +> when reverting, prefer reverse-editing only the mutated line rather than > `git checkout -- ` if you have uncommitted unit tests in the same file — > a whole-file checkout would discard them too. From 1e5900cb9809e371569a3527394e8b603521e62d Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jun 2026 14:09:34 +0800 Subject: [PATCH 26/31] fix: remove notes --- README.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/README.md b/README.md index 6675d3e2..1c0e2b22 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,6 @@ cargo install cargo-fuzz cargo install just # optional but recommended ``` -> [!NOTE] -> **Why nightly?** `cargo-fuzz` passes `-Zsanitizer=address` and -> `-Zinstrument-coverage` (unstable flags) to `rustc`, and depends on the -> `llvm-tools-preview` nightly component for coverage reporting. -> `rust-toolchain.toml` pins the whole repository to nightly, so you never -> need an explicit `+nightly` flag. - ### 2. Setup ```bash @@ -140,14 +133,6 @@ just fuzz-props Each target lives at `fuzz/fuzz_targets/.rs`. -> [!NOTE] -> **Input-independent checks are not fuzz targets here.** Deterministic invariants -> that ignore their input (e.g. genesis-account contents, getter/round-trip -> identities, the system-account-modification guard) belong in `logos-execution-zone` -> unit tests, not the fuzz corpus. See -> [`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md) for the policy and -> the mutant→test mapping. - --- ## 🧬 Corpus Management @@ -216,13 +201,6 @@ GitHub Actions runs these workflows on every push/PR and nightly: | `mutants.yml` | Mutation testing (`cargo-mutants`) | | `lint.yml` | Formatting + Clippy | -> [!NOTE] -> All **20** libFuzzer targets are wired into every `fuzz.yml` matrix -> (smoke-fuzz · regression · perf-baseline), the `fuzz-afl.yml` AFL++ lane, and -> the `mutants.yml` corpus-replay job. New targets are added automatically by -> `just new-target`; see [`docs/fuzzing.md`](docs/fuzzing.md) for the manual -> fallback instructions. - --- ## 📖 Documentation From 030774b8fd5d2085df8e48e027a3686bb4f47846 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jun 2026 14:11:52 +0800 Subject: [PATCH 27/31] fix: remove work done items --- current_vs_alternative_approach.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/current_vs_alternative_approach.md b/current_vs_alternative_approach.md index e20982c8..f4923662 100644 --- a/current_vs_alternative_approach.md +++ b/current_vs_alternative_approach.md @@ -143,19 +143,6 @@ fuzzing replacement. ## 🧭 Decision-maker Recommendations -**Already done** (was previously recommended here): - -- ✅ **AFL++ parallel lane** — `just fuzz-afl` + `fuzz-afl.yml`. -- ✅ **`cargo-mutants`** — `just mutants-harness` / `mutants-protocol` + `mutants.yml`, - with the Plane A / Plane B framework documented in - [`docs/mutants-not-fuzzable.md`](docs/mutants-not-fuzzable.md). -- ✅ **Differential testing** — `fuzz_sequencer_vs_replayer`. -- ✅ **Complete `fuzz.yml` CI matrix** — all **20** libFuzzer targets now run in - every `fuzz.yml` matrix (smoke-fuzz · regression · perf-baseline) and the - `fuzz-afl.yml` AFL++ lane, including `fuzz_merkle_tree`, - `fuzz_transaction_properties`, `fuzz_privacy_preserving_witness`, - `fuzz_encoding_privacy_preserving`, and `fuzz_nullifier_set_roundtrip`. - **Remaining higher-ROI next steps, in priority order:** 1. **Honggfuzz on Linux CI only** — hardware-counter coverage finds different paths; From 7ad99bfcbf04452755bbbb63e5ef8d61cbc9b35e Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jun 2026 14:22:12 +0800 Subject: [PATCH 28/31] fix: remove work done items --- docs/fuzzing.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/fuzzing.md b/docs/fuzzing.md index a714da0e..56943248 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -693,6 +693,4 @@ from `data`; if a check doesn't depend on the input, write it as a unit test in |------|-------| | `PrivacyPreservingTransaction` coverage | Excluded from `fuzz_encoding_roundtrip` because its ZK receipt cannot be reconstructed in a fuzzing loop. A dedicated slow target with `RISC0_DEV_MODE=1` and `proptest` should be added after the current targets are stable | | `fuzz_validate_execute_consistency` new-account detection | If `execute_check_on_state` creates a brand-new account absent from both the genesis set and the diff, that state-widening will not be detected — full detection requires iterating all accounts in `V03State`, which the API does not currently expose | -| Differential testing (sequencer vs replayer) | ✅ Implemented — `fuzz_sequencer_vs_replayer` feeds the same block through the sequencer path (`validate_on_state` → `apply_state_diff`) and the replayer path (`execute_check_on_state`) and asserts identical state for all known accounts | -| AFL++ integration | ✅ Implemented — `just afl-build`, `just fuzz-afl`, `just fuzz-afl-parallel`; nightly CI in `.github/workflows/fuzz-afl.yml`; single `fuzz/Cargo.toml` covers both engines via feature flags | | LEZ version tracking | There is no submodule pin — `lez-fuzzing` reads `../logos-execution-zone` as checked out. Update that repo to a release tag or a tested commit, then run `just update-lez` (which does `git pull --ff-only`) and open a PR to bump it | From 4f06e820d58cc5bfb31017ffa14a63bb74792493 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 12 Jun 2026 14:30:19 +0800 Subject: [PATCH 29/31] fix: use Logos icon --- README.md | 2 +- logos.avif | Bin 0 -> 1093 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 logos.avif diff --git a/README.md b/README.md index 1c0e2b22..60b3dd55 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-# 🦀 Lez-fuzzing +# Lez-fuzzing **Coverage-guided fuzzing & adversarial testing infrastructure for the [Logos Execution Zone (LEZ)](https://github.com/) protocol.** diff --git a/logos.avif b/logos.avif new file mode 100644 index 0000000000000000000000000000000000000000..59d4db741e86776aae1f7e8bc583ebc5573e59a8 GIT binary patch literal 1093 zcmZQzV30{GsVqn=%S>Ycg51nBLl8SRGZDyVEXYkQNd$=lfnr8VP7#F3z)+BxTmoam zXug8Xl3Xx{5lEV3=Hw@XcrFeMObj3q2F586nt_2y7$^n=%*$aS4D*0oi_FYCurt82 zqCie6SQ$`vVwr&;?EJ)4=(maV}hR%%&V7(%l#RaJ#oj_0l z#R3JH6`3FbW)2PxAO%s+AOjKt`T`=y2$Hc#EUj<`36~b-S>+a_rh^G5tK`h&0;}T6 z;*!+dVk@AU%$&@`qDrg8oPrE+c*$lKIU9 z2h^McQUVGHrk%Mdi6ue|T$~n?yT2zg^0G}ZYGx5Uz{H`@=x}$2#s7}3+o2~sgr!W( z&M3#;T`TG}`GfVBk9D&j=EnS-Hsxsg!Hw^@Ja6A9YrOt+-jq#~Ew0|XBDrg>@apx7 zotjyG1?M^p%2WzAtd(K=xFK?jrS_Fq^Bdo>iCVO2UDrFp#CyTxw8bNrnXBW@JSmWQ zCb?!mC;PL5`vR}oO}f`pWT=pFCDh4Dr)^!H)R#|dj;OqSC*8RCho)fXF^j#t`9c(QuYBOm6R-wggKv1>k;N5$=mi4gHAU-QdrV)>ityJZ}E z?R1uWU0z`Rntws;a{Jc(3fDqB47_IlUc0(L=BnQWWH z`rdTSc}^ZRosZv3*|U!A3Nf-<*jaJT?}@iXPO~`c>DI-~yO!Nm;ap{$yrj6tMQ(A$ zbZI}in}@6g4nF;9q-q}T<)FMiRb+*f-ow>PH_6!AB~*WJP5P!{w7Wr?cf0C5iKNB9 z>^6CD_sq=o)D37Y{CIxPGQWkdD`wy0mz(LZ_r|`CJb&ZE?S{$r2|`i(ceTlVN>~~N zOcHDopd=AsG?7v80Ea@rT?3_)r-Z{d#ow~t9KgMuvGr!yt)_YLcl!&S8wyU{73sh5 zCc^H5Uf1lo`Z7hm9;eOSPN^o!iD`IuEsE^ew{7=xORv24w=AolH58UaNXs Date: Mon, 15 Jun 2026 10:20:20 +0800 Subject: [PATCH 30/31] fix: sync with latest LEZ --- fuzz/fuzz_targets/fuzz_block_verification.rs | 13 ++++++------ .../fuzz_encoding_privacy_preserving.rs | 20 +++++++------------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_block_verification.rs b/fuzz/fuzz_targets/fuzz_block_verification.rs index 0973beaf..9e7c8572 100644 --- a/fuzz/fuzz_targets/fuzz_block_verification.rs +++ b/fuzz/fuzz_targets/fuzz_block_verification.rs @@ -33,10 +33,9 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { let base = wrap.0; let signing_key = PrivateKey::try_new(DUMMY_KEY_BYTES).expect("constant key is valid"); - let bedrock = [0u8; 32]; // Compute the canonical hash for the base input. - let block = base.clone().into_pending_block(&signing_key, bedrock); + let block = base.clone().into_pending_block(&signing_key); let hash_base = block.header.hash; // ── INVARIANT 1: HashableBlockData::from(Block) is lossless ────────────────── @@ -51,7 +50,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { { let roundtrip_hashable = HashableBlockData::from(block); let hash_roundtrip = roundtrip_hashable - .into_pending_block(&signing_key, bedrock) + .into_pending_block(&signing_key) .header .hash; assert_eq!( @@ -67,7 +66,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { { let mut m = base.clone(); m.block_id = m.block_id.wrapping_add(1); - let hash_m = m.into_pending_block(&signing_key, bedrock).header.hash; + let hash_m = m.into_pending_block(&signing_key).header.hash; assert_ne!( hash_base, hash_m, @@ -81,7 +80,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { { let mut m = base.clone(); m.prev_block_hash.0[0] ^= 0xFF; - let hash_m = m.into_pending_block(&signing_key, bedrock).header.hash; + let hash_m = m.into_pending_block(&signing_key).header.hash; assert_ne!( hash_base, hash_m, @@ -95,7 +94,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { { let mut m = base.clone(); m.timestamp = m.timestamp.wrapping_add(1); - let hash_m = m.into_pending_block(&signing_key, bedrock).header.hash; + let hash_m = m.into_pending_block(&signing_key).header.hash; assert_ne!( hash_base, hash_m, @@ -121,7 +120,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { if first != last { let mut reordered = base.clone(); reordered.transactions.reverse(); - let hash_reordered = reordered.into_pending_block(&signing_key, bedrock).header.hash; + let hash_reordered = reordered.into_pending_block(&signing_key).header.hash; assert_ne!( hash_base, hash_reordered, diff --git a/fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs b/fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs index 26defc5c..485d5a35 100644 --- a/fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs +++ b/fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs @@ -107,20 +107,16 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { ); } - // ── INVARIANT [LengthMatchAccepted] ─────────────────────────────────────── - // When public_keys.len() == ciphertexts.len() == 0, `try_from_circuit_output` - // must succeed. - // - // Original check: `if public_keys.len() != output.ciphertexts.len() { Err }` - // With mutation `!=` → `==`: `if 0 == 0` → `true` → Err is returned. - // Our assertion that the call SUCCEEDS catches the mutation. + // ── INVARIANT [CircuitOutputAccepted] ───────────────────────────────────── + // `try_from_circuit_output` must succeed for a well-formed (empty) circuit + // output, mapping the output fields onto the resulting `Message`. { let empty_output = PrivacyPreservingCircuitOutput { public_pre_states: vec![], public_post_states: vec![], new_commitments: vec![], new_nullifiers: vec![], - ciphertexts: vec![], + encrypted_private_post_states: vec![], block_validity_window: BlockValidityWindow::new_unbounded(), timestamp_validity_window: TimestampValidityWindow::new_unbounded(), }; @@ -128,15 +124,13 @@ fuzz_props::fuzz_entry!(|data: &[u8]| { let result = PPMessage::try_from_circuit_output( vec![], // public_account_ids vec![], // nonces - vec![], // public_keys (0 entries) empty_output, ); assert!( result.is_ok(), - "INVARIANT VIOLATION [LengthMatchAccepted]: \ - try_from_circuit_output must accept when keys(0) == ciphertexts(0), \ - got: {:?} — \ - possible mutation: != changed to == in the length check", + "INVARIANT VIOLATION [CircuitOutputAccepted]: \ + try_from_circuit_output must accept a well-formed empty output, \ + got: {:?}", result.err(), ); } From 830318749cedf2951d57086577872e63109a47bd Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 15 Jun 2026 13:24:18 +0800 Subject: [PATCH 31/31] fix: alight fuzz-afl-parallel recipe with fuzz-parallel --- Justfile | 105 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/Justfile b/Justfile index 50cab0f8..0d84bf98 100644 --- a/Justfile +++ b/Justfile @@ -349,45 +349,96 @@ fuzz-afl TARGET="" TIME="30": echo " Format for a report: just afl-fmt " fi -# Run AFL++ with N parallel instances (1 main + N-1 secondary) for TIME seconds. -# Requires that afl-fuzz is on PATH; all instances share afl-output/{{TARGET}}/. -# On macOS the crash reporter is disabled automatically for the duration of the -# run and re-enabled when the script exits. +# Run AFL++ over all targets, up to JOBS targets at a time (one instance each), +# for TIME seconds per target. As soon as one target finishes the next queued +# target is launched, so the pool stays full until every target is done. +# Requires that afl-fuzz is on PATH. +# On macOS this needs the System V shared-memory limits raised and the crash +# reporter disabled (both require admin). If the current user can use sudo the +# recipe does it automatically; otherwise it asks an administrator to run +# `sudo afl-system-config` once (sets the shm limits and disables the reporter — +# both persist until reboot) and exits. # -# Usage: just fuzz-afl-parallel fuzz_state_transition -# just fuzz-afl-parallel fuzz_state_transition 8 600 -fuzz-afl-parallel TARGET WORKERS="4" TIME="300": +# Usage: just fuzz-afl-parallel # all targets, 30 s each, 4 at a time +# just fuzz-afl-parallel 600 # all targets, 600 s each, 4 at a time +# just fuzz-afl-parallel 600 8 # all targets, 600 s each, 8 at a time +fuzz-afl-parallel TIME="30" JOBS="4": #!/bin/bash set -euo pipefail - BINARY="fuzz/target/release/{{TARGET}}" - CORPUS="corpus/libfuzz/{{TARGET}}" # seed from libFuzzer corpus - OUTPUT="afl-output/{{TARGET}}" - mkdir -p "$CORPUS" "$OUTPUT" - if [ ! -f "$BINARY" ]; then - echo "Binary not found — building first…" - just afl-build-target {{TARGET}} - fi - # ── macOS: disable crash reporter for the duration of this run ─────────── + + # ── Collect targets to run ──────────────────────────────────────────────── + TARGETS=($(cargo fuzz list 2>/dev/null)) + + # ── macOS: shared-memory limits + crash reporter (both need admin) ──────── if [ "$(uname)" = "Darwin" ]; then SL=/System/Library; PL=com.apple.ReportCrash + # Can this user run privileged commands without prompting for a password? + if sudo -n true 2>/dev/null; then CAN_SUDO=1; else CAN_SUDO=0; fi + + # The default kern.sysv.shmmax (4 MB) is too small for AFL++'s shm + # segments, so shmget() fails with EINVAL under parallel fuzzing. + NEED_SHMMAX=524288000 + CUR_SHMMAX=$(sysctl -n kern.sysv.shmmax 2>/dev/null || echo 0) + if [ "$CUR_SHMMAX" -lt "$NEED_SHMMAX" ]; then + if [ "$CAN_SUDO" = 1 ]; then + echo "macOS: raising shared-memory limits for parallel AFL++ instances…" + sudo sysctl kern.sysv.shmmax=524288000 kern.sysv.shmmin=1 \ + kern.sysv.shmseg=48 kern.sysv.shmall=131072000 \ + kern.sysv.shmmni=1024 >/dev/null || true + else + echo "ERROR: System V shared-memory limits are too low for parallel AFL++" + echo " (kern.sysv.shmmax=$CUR_SHMMAX, need >= $NEED_SHMMAX) and this" + echo " user cannot use sudo." + echo "" + echo " Ask an administrator to run this once (persists until reboot):" + echo "" + echo " sudo afl-system-config" + echo "" + echo " It raises the shm limits and disables the crash reporter." + echo " Then re-run: just fuzz-afl-parallel {{TIME}} {{JOBS}}" + exit 1 + fi + fi + + # Disable the crash reporter for the duration of the run. The per-user + # agent needs no privileges; the system daemon does, so only attempt it + # when we can sudo (an admin's `afl-system-config` covers it otherwise). echo "macOS: disabling crash reporter (AFL++ requirement)…" - launchctl unload -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true - sudo launchctl unload -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + launchctl unload -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true + if [ "$CAN_SUDO" = 1 ]; then + sudo launchctl unload -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + fi trap ' echo "Re-enabling macOS crash reporter…" SL=/System/Library; PL=com.apple.ReportCrash - launchctl load -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true - sudo launchctl load -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + launchctl load -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true + [ "'"$CAN_SUDO"'" = 1 ] && sudo launchctl load -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true ' EXIT fi - # Main instance - afl-fuzz -M main -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" & - # Secondary instances - for i in $(seq 1 $(( {{WORKERS}} - 1 ))); do - afl-fuzz -S "secondary${i}" -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" & + + # ── Run targets, up to JOBS at a time ───────────────────────────────────── + _run_one() { + local t="$1" + local BINARY="fuzz/target/release/$t" + local CORPUS="corpus/libfuzz/$t" # seed from libFuzzer corpus + local OUTPUT="afl-output/$t" + mkdir -p "$CORPUS" "$OUTPUT" + if [ ! -f "$BINARY" ]; then + echo "Binary not found — building $t first…" + just afl-build-target "$t" + fi + timeout {{TIME}} afl-fuzz -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true + } + echo "Targets: ${#TARGETS[@]} | max parallel: {{JOBS}} | time per target: {{TIME}}s" + for t in "${TARGETS[@]}"; do + # Wait for a free slot (block until running jobs < JOBS) + while [ "$(jobs -rp | wc -l | tr -d ' ')" -ge "{{JOBS}}" ]; do + wait -n 2>/dev/null || sleep 0.5 + done + echo "=== launching afl++ $t ({{TIME}}s) ===" + _run_one "$t" & done - sleep {{TIME}} - kill $(jobs -p) 2>/dev/null || true + # Drain the remaining running targets wait 2>/dev/null || true just afl-corpus-sync