From 56162a66a3eb202000c116bcab481c354625e9c1 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 22 Jun 2026 10:17:30 +0800 Subject: [PATCH] fix: repo pinning - minor invariant changes - missing target in CI added - docs --- .github/actions/checkout-lez/action.yml | 20 ++++++ .github/workflows/fuzz-afl.yml | 1 + .github/workflows/fuzz.yml | 13 +++- .github/workflows/lez-compat.yml | 53 ++++++++++++++ .github/workflows/mutants.yml | 3 +- Justfile | 11 ++- README.md | 5 +- docs/fuzzing.md | 13 ++-- fuzz_props/src/generators.rs | 15 +++- fuzz_props/src/privacy.rs | 6 +- fuzz_props/src/tests/privacy.rs | 18 +++-- scripts/add_fuzz_target.py | 9 +++ scripts/check_target_inventory.py | 92 +++++++++++++++++++++++++ scripts/mutants-corpus-test.sh | 1 + 14 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/lez-compat.yml create mode 100755 scripts/check_target_inventory.py diff --git a/.github/actions/checkout-lez/action.yml b/.github/actions/checkout-lez/action.yml index 435920a4..1f949cf9 100644 --- a/.github/actions/checkout-lez/action.yml +++ b/.github/actions/checkout-lez/action.yml @@ -4,6 +4,18 @@ description: > symlinks it to the expected sibling path (../logos-execution-zone) so that Cargo path dependencies resolve correctly. +inputs: + ref: + description: > + Git ref (SHA, tag, or branch) of logos-execution-zone to check out. + Defaults to the pinned, reviewed revision below — this single line is the + source of truth for the LEZ version every PR-gating workflow builds against. + To bump it: update ../logos-execution-zone to a tested commit, run + `just update-lez`, replace this SHA, and open a PR. The scheduled + lez-compat workflow overrides this with `main` to detect upstream drift. + required: false + default: dac429a94af932b0c827544fff8b9de85b83e6f3 + runs: using: composite steps: @@ -11,8 +23,16 @@ runs: uses: actions/checkout@v4 with: repository: logos-blockchain/logos-execution-zone + ref: ${{ inputs.ref }} path: logos-execution-zone - name: Symlink logos-execution-zone to sibling directory run: ln -s "$GITHUB_WORKSPACE/logos-execution-zone" "$GITHUB_WORKSPACE/../logos-execution-zone" shell: bash + + - name: Report pinned LEZ revision + run: | + echo "LEZ ref: ${{ inputs.ref }}" + echo "LEZ SHA: $(git -C "$GITHUB_WORKSPACE/logos-execution-zone" rev-parse HEAD)" \ + | tee -a "$GITHUB_STEP_SUMMARY" + shell: bash diff --git a/.github/workflows/fuzz-afl.yml b/.github/workflows/fuzz-afl.yml index d419242f..dde2a9e7 100644 --- a/.github/workflows/fuzz-afl.yml +++ b/.github/workflows/fuzz-afl.yml @@ -46,6 +46,7 @@ jobs: fuzz_privacy_preserving_witness fuzz_encoding_privacy_preserving fuzz_nullifier_set_roundtrip + fuzz_privacy_preserving_state_transition EOF ) echo "targets=$targets" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 4970f7b0..2d8716b9 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -12,6 +12,14 @@ env: CARGO_TERM_COLOR: always jobs: + # ── Target-inventory gate: fail if any workflow/script/doc omits a target ──── + target-inventory: + name: Target inventory in sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: python3 scripts/check_target_inventory.py + # ── Smoke fuzz: 60 s per target ───────────────────────────────────────────── smoke-fuzz: name: Smoke fuzz (${{ matrix.target }}) @@ -40,6 +48,7 @@ jobs: - fuzz_privacy_preserving_witness - fuzz_encoding_privacy_preserving - fuzz_nullifier_set_roundtrip + - fuzz_privacy_preserving_state_transition steps: - uses: actions/checkout@v4 @@ -228,6 +237,7 @@ jobs: - fuzz_privacy_preserving_witness - fuzz_encoding_privacy_preserving - fuzz_nullifier_set_roundtrip + - fuzz_privacy_preserving_state_transition steps: - uses: actions/checkout@v4 - name: Checkout logos-execution-zone @@ -300,7 +310,8 @@ jobs: fuzz_transaction_properties \ fuzz_privacy_preserving_witness \ fuzz_encoding_privacy_preserving \ - fuzz_nullifier_set_roundtrip; do + fuzz_nullifier_set_roundtrip \ + fuzz_privacy_preserving_state_transition; 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/.github/workflows/lez-compat.yml b/.github/workflows/lez-compat.yml new file mode 100644 index 00000000..6e8f8cd3 --- /dev/null +++ b/.github/workflows/lez-compat.yml @@ -0,0 +1,53 @@ +name: LEZ upstream compatibility + +# Detects drift between this fuzzing repo and the *latest* logos-execution-zone +# main, independently of PR gating (which builds against the pinned SHA in +# .github/actions/checkout-lez). A failure here means upstream main moved in a +# way that breaks the harness — bump the pinned ref once the break is resolved. +on: + schedule: + - cron: "0 4 * * *" + push: + branches: [ test-privacy-features ] + workflow_dispatch: + +env: + RISC0_DEV_MODE: "1" + CARGO_TERM_COLOR: always + +jobs: + build-against-upstream-main: + name: Build + test against LEZ main + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Checkout logos-execution-zone (main, not the pinned SHA) + uses: ./.github/actions/checkout-lez + with: + ref: main + + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@nightly + with: + components: llvm-tools-preview + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: lez-compat-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Install logos-blockchain-circuits + uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build shared fuzz harness against upstream main + run: cargo build -p fuzz_props --release + + - name: Run fuzz_props tests against upstream main + run: cargo test -p fuzz_props --release diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index 44bdd251..8ac9f7ef 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -164,7 +164,8 @@ 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; do + fuzz_encoding_privacy_preserving fuzz_nullifier_set_roundtrip \ + fuzz_privacy_preserving_state_transition; do cargo fuzz build "${target}" done diff --git a/Justfile b/Justfile index 0d84bf98..4dd33316 100644 --- a/Justfile +++ b/Justfile @@ -299,7 +299,9 @@ fuzz-afl TARGET="" TIME="30": echo "Binary not found — building $t first…" just afl-build-target "$t" fi - timeout "$TIME" afl-fuzz -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true + # Use afl-fuzz's own -V (run for N seconds then exit) instead of GNU + # `timeout`, which is not installed by default on macOS. + afl-fuzz -V "$TIME" -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true } for t in "${TARGETS[@]}"; do echo "=== afl++ $t for ${TIME}s ===" @@ -427,7 +429,9 @@ fuzz-afl-parallel TIME="30" JOBS="4": echo "Binary not found — building $t first…" just afl-build-target "$t" fi - timeout {{TIME}} afl-fuzz -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true + # Use afl-fuzz's own -V (run for N seconds then exit) instead of GNU + # `timeout`, which is not installed by default on macOS. + afl-fuzz -V {{TIME}} -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true } echo "Targets: ${#TARGETS[@]} | max parallel: {{JOBS}} | time per target: {{TIME}}s" for t in "${TARGETS[@]}"; do @@ -462,7 +466,8 @@ afl-corpus-sync: [ -d "$QUEUE" ] || continue for f in "$QUEUE"/id:*; do [ -f "$f" ] || continue - HASH=$(sha1sum "$f" | cut -d' ' -f1) + # sha1sum (GNU/Linux) is absent on stock macOS; fall back to shasum. + HASH=$( { sha1sum "$f" 2>/dev/null || shasum -a 1 "$f"; } | cut -d' ' -f1) DEST_FILE="${DEST}/${HASH}" if [ ! -f "$DEST_FILE" ]; then cp "$f" "$DEST_FILE" diff --git a/README.md b/README.md index ef852904..d2544640 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [Logos Execution Zone (LEZ)](https://github.com/logos-blockchain/logos-execution-zone) protocol.** [![Rust](https://img.shields.io/badge/rust-nightly-orange?logo=rust)](rust-toolchain.toml) -[![Fuzzing](https://img.shields.io/badge/libFuzzer%20%C2%B7%20AFL%2B%2B-20%20targets-blue)](#-fuzz-targets) +[![Fuzzing](https://img.shields.io/badge/libFuzzer%20%C2%B7%20AFL%2B%2B-21%20targets-blue)](#-fuzz-targets) [![Mutation testing](https://img.shields.io/badge/cargo--mutants-enabled-green)](.github/workflows/mutants.yml) [![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE-MIT) @@ -31,7 +31,7 @@ lez-fuzzing/ │ └── generators.rs # Arbitrary / proptest strategies ├── fuzz/ # cargo-fuzz crate (own [workspace] sentinel) │ ├── Cargo.toml -│ ├── fuzz_targets/ # 20 targets total — see table below +│ ├── fuzz_targets/ # 21 targets total — see table below │ │ ├── _template.rs # Template for `just new-target` │ │ └── fuzz_*.rs │ └── corpus/ # Curated seed inputs (one dir per target) @@ -130,6 +130,7 @@ just fuzz-props | 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) | +| 21 | `fuzz_privacy_preserving_state_transition` | Path B — `NSSATransaction::PrivacyPreserving` through `execute_check_on_state` with a dev-mode passing proof: reaches commitment/nullifier checks 5–6 + `apply_state_diff`. Asserts no-panic, StateIsolationOnFailure, PrivateStateIsolationOnFailure, CommitmentInsertion, NonceIncrementCorrectness, PostStateApplied, ReplayRejection (balance conservation intentionally not asserted — the fake proof bypasses the circuit guarantee) | Each target lives at `fuzz/fuzz_targets/.rs`. diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 56943248..b1f95e7d 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -129,6 +129,7 @@ just fuzz-regression | `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` | +| `fuzz_privacy_preserving_state_transition` | Path B — `NSSATransaction::PrivacyPreserving` through `execute_check_on_state` with a dev-mode passing proof, reaching checks 5–6 (`check_commitments_are_new` / `check_nullifiers_are_valid`) and `apply_state_diff`: **No panic**, **StateIsolationOnFailure**, **PrivateStateIsolationOnFailure**, **CommitmentInsertion**, **NonceIncrementCorrectness**, **PostStateApplied**, **ReplayRejection**. Balance conservation is intentionally *not* asserted — the synthesised fake receipt bypasses the circuit guarantee. Requires `RISC0_DEV_MODE=1` | `fuzz/fuzz_targets/fuzz_privacy_preserving_state_transition.rs` | --- @@ -376,8 +377,8 @@ The nightly AFL++ CI workflow has two jobs: | Job | Triggers | Matrix | |-----|----------|--------| -| `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 | +| `afl-smoke` | nightly + `workflow_dispatch` | all 21 targets, 60 s each | +| `afl-coverage-aggregate` | nightly, `needs: afl-smoke` | all 21 targets merged into one LLVM HTML report | The smoke job (one matrix leg per target, on `ubuntu-latest`): 1. Builds AFL++ from source, then builds the target with `cargo afl build --no-default-features --features fuzzer-afl` @@ -387,7 +388,7 @@ The smoke job (one matrix leg per target, on `ubuntu-latest`): The coverage-aggregate job: 1. Downloads every smoke leg's findings -2. Rebuilds all 20 targets with `RUSTFLAGS="-C instrument-coverage"` +2. Rebuilds all 21 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` @@ -620,6 +621,7 @@ Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`: | `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)* | +| `fuzz_privacy_preserving_state_transition` | slow — dev-mode proof synthesis + verification per exec dominates runtime *(estimate)* | > [!NOTE] > Throughput figures for the five new targets are rough estimates; run `just perf-baseline` @@ -691,6 +693,7 @@ from `data`; if a check doesn't depend on the input, write it as a unit test in | Item | Notes | |------|-------| -| `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 | +| `PrivacyPreservingTransaction` encoding | Still excluded from `fuzz_encoding_roundtrip` because a real ZK receipt cannot be reconstructed in a fast fuzzing loop. Encoding is instead covered by the dedicated `fuzz_encoding_privacy_preserving` target | +| `PrivacyPreservingTransaction` state transition (fake-receipt caveat) | Covered by `fuzz_privacy_preserving_state_transition`, which drives `execute_check_on_state` under `RISC0_DEV_MODE=1` using a synthesised *passing* proof (a dev-mode fake receipt). Because the proof is forced to pass, the circuit's balance-conservation guarantee is bypassed, so this target intentionally does **not** assert balance conservation — see `fuzz_props::privacy::synthesize_passing_proof` for the binding caveat | | `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 | -| 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 | +| LEZ version tracking | CI pins the upstream revision in a single place — the `ref` input default of `.github/actions/checkout-lez`. PR-gating workflows build against that pinned SHA for reproducibility; the scheduled `lez-compat.yml` workflow overrides it with `main` to flag upstream drift. Locally, `lez-fuzzing` still reads `../logos-execution-zone` as checked out. To bump: update that repo to a tested commit, run `just update-lez` (which does `git pull --ff-only`), replace the SHA in the action, and open a PR | diff --git a/fuzz_props/src/generators.rs b/fuzz_props/src/generators.rs index d11df5a9..3aec1ccd 100644 --- a/fuzz_props/src/generators.rs +++ b/fuzz_props/src/generators.rs @@ -84,6 +84,9 @@ pub fn arbitrary_fuzz_state(u: &mut Unstructured<'_>) -> arbitrary::Result, accounts: &[FuzzAccount], @@ -93,12 +96,20 @@ pub fn arb_fuzz_native_transfer( } let from_idx = (u8::arbitrary(u)? as usize) % accounts.len(); let to_idx = (u8::arbitrary(u)? as usize) % accounts.len(); - let nonce = u128::arbitrary(u)?; - let amount = u128::arbitrary(u)?; let from = &accounts[from_idx]; let to = &accounts[to_idx]; + let (nonce, amount) = if bool::arbitrary(u)? { + // Biased valid: nonce near the genesis value, amount within balance. + let nonce = u128::from(u8::arbitrary(u)?) % 4; // 0..=3 + let amount = u128::arbitrary(u)? % from.balance.saturating_add(1); // 0..=balance + (nonce, amount) + } else { + // Adversarial: full range drives the rejection paths. + (u128::arbitrary(u)?, u128::arbitrary(u)?) + }; + Ok( common::test_utils::create_transaction_native_token_transfer( from.account_id, diff --git a/fuzz_props/src/privacy.rs b/fuzz_props/src/privacy.rs index 4bd2e591..180700d7 100644 --- a/fuzz_props/src/privacy.rs +++ b/fuzz_props/src/privacy.rs @@ -241,8 +241,10 @@ pub fn arb_privacy_preserving_tx( } } - // ── public_post_states (length varied to exercise the apply/zip-truncation path) ── - let n_post = (u8::arbitrary(u)? as usize) % (public_account_ids.len() + 1); + // ── public_post_states ── + // Range 0..=len+3 so lengths can exceed the public-account count, exercising + // both the truncation path and the oversized/length-mismatch path. + let n_post = (u8::arbitrary(u)? as usize) % (public_account_ids.len() + 4); let public_post_states = std::iter::repeat_with(|| arb_account(u)) .take(n_post) .collect::>>()?; diff --git a/fuzz_props/src/tests/privacy.rs b/fuzz_props/src/tests/privacy.rs index 901000e6..ad4a0d08 100644 --- a/fuzz_props/src/tests/privacy.rs +++ b/fuzz_props/src/tests/privacy.rs @@ -280,6 +280,7 @@ fn arb_privacy_preserving_tx_generator_invariants() { let mut max_commitments = 0_usize; let mut max_nullifiers = 0_usize; let mut saw_empty_comm_nonempty_null = false; + let mut saw_oversize_post_states = false; let mut garbage = 0_usize; let mut saw_garbage = false; @@ -308,14 +309,16 @@ fn arb_privacy_preserving_tx_generator_invariants() { // The signer count is drawn modulo `max_signers + 1`, so it can never exceed // the cap of 3 distinct signers. assert!(n_signers <= 3, "n_signers {n_signers} exceeds the cap of 3"); - // Post-states are drawn modulo `public_account_ids.len() + 1`, so there is - // never a post-state without a corresponding public account. + // Post-states are drawn modulo `public_account_ids.len() + 4` (0..=len+3). assert!( - msg.public_post_states.len() <= msg.public_account_ids.len(), - "public_post_states {} exceeds public_account_ids {}", + msg.public_post_states.len() <= msg.public_account_ids.len() + 3, + "public_post_states {} exceeds public_account_ids + 3 ({})", msg.public_post_states.len(), - msg.public_account_ids.len() + msg.public_account_ids.len() + 3 ); + if msg.public_post_states.len() > msg.public_account_ids.len() { + saw_oversize_post_states = true; + } // At most 3 signers plus at most 3 extra ids (both deduplicated). assert!( msg.public_account_ids.len() <= 6, @@ -406,6 +409,11 @@ fn arb_privacy_preserving_tx_generator_invariants() { saw_empty_comm_nonempty_null, "the generator never produced empty commitments with non-empty nullifiers" ); + // The oversized shape (more post-states than public account ids) must be reachable. + assert!( + saw_oversize_post_states, + "the generator never produced more post-states than public account ids" + ); // The garbage-proof branch (~1 in 8) must be reachable at all. assert!(saw_garbage, "the generator never produced a garbage proof"); // The garbage-proof rate must sit near the intended 1/8. Integer bands avoid float diff --git a/scripts/add_fuzz_target.py b/scripts/add_fuzz_target.py index 3dde8838..97a75318 100644 --- a/scripts/add_fuzz_target.py +++ b/scripts/add_fuzz_target.py @@ -196,6 +196,15 @@ def main() -> None: print() print(" 4. Run with libFuzzer: just fuzz-one", target) print(" Run with AFL++: just fuzz-afl", target) + print() + print(" 5. This script only edits .github/workflows/fuzz.yml. Add the") + print(" target to the other enumeration sites too, then verify with:") + print(" python3 scripts/check_target_inventory.py") + print(" (the same check runs in CI and will fail the build on drift):") + print(" .github/workflows/fuzz-afl.yml") + print(" .github/workflows/mutants.yml") + print(" scripts/mutants-corpus-test.sh") + print(" README.md, docs/fuzzing.md") if __name__ == "__main__": diff --git a/scripts/check_target_inventory.py b/scripts/check_target_inventory.py new file mode 100755 index 00000000..7c53ff02 --- /dev/null +++ b/scripts/check_target_inventory.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Fail if any fuzz target registered in fuzz/Cargo.toml is missing from a +workflow / script / doc that enumerates the target list. + +`fuzz/Cargo.toml` is the single source of truth: every `[[bin]] name = "fuzz_*"` +must be mentioned by name in each of the consumer files below. This guards +against the drift that `scripts/add_fuzz_target.py` cannot prevent on its own +(it only edits `.github/workflows/fuzz.yml`). + +Usage: + python3 scripts/check_target_inventory.py + +Exit code 0 = all consumers list every target; 1 = drift detected (prints the +missing target/file pairs). Run from anywhere; paths are resolved relative to +the repository root. +""" + +import re +import sys +from pathlib import Path + +# Files that enumerate the full target list and must stay in sync with Cargo.toml. +# Paths are relative to the repository root. +CONSUMERS = [ + ".github/workflows/fuzz.yml", + ".github/workflows/fuzz-afl.yml", + ".github/workflows/mutants.yml", + "scripts/mutants-corpus-test.sh", + "README.md", + "docs/fuzzing.md", +] + +_BIN_NAME_RE = re.compile(r'name\s*=\s*"(fuzz_[a-z0-9_]+)"') + + +def registered_targets(cargo_toml: Path) -> list[str]: + """Every `[[bin]] name = "fuzz_*"` in fuzz/Cargo.toml, in file order.""" + names = _BIN_NAME_RE.findall(cargo_toml.read_text()) + # Preserve order, drop duplicates defensively. + seen: set[str] = set() + ordered: list[str] = [] + for n in names: + if n not in seen: + seen.add(n) + ordered.append(n) + return ordered + + +def main() -> None: + root = Path(__file__).parent.parent # repository root + cargo_toml = root / "fuzz" / "Cargo.toml" + if not cargo_toml.exists(): + print(f"ERROR: {cargo_toml} not found", file=sys.stderr) + sys.exit(1) + + targets = registered_targets(cargo_toml) + if not targets: + print(f"ERROR: no [[bin]] targets found in {cargo_toml}", file=sys.stderr) + sys.exit(1) + + missing: list[tuple[str, str]] = [] + for rel in CONSUMERS: + path = root / rel + if not path.exists(): + print(f"ERROR: consumer file not found: {rel}", file=sys.stderr) + sys.exit(1) + text = path.read_text() + for target in targets: + if target not in text: + missing.append((rel, target)) + + if missing: + print( + f"Target-inventory drift: {len(targets)} targets registered in " + f"fuzz/Cargo.toml, but some consumers are missing entries:\n", + file=sys.stderr, + ) + for rel, target in missing: + print(f" MISSING {rel} -> {target}", file=sys.stderr) + print( + "\nAdd the target(s) above to each listed file " + "(see scripts/add_fuzz_target.py for the canonical insertion points).", + file=sys.stderr, + ) + sys.exit(1) + + print(f"OK: all {len(CONSUMERS)} consumers list every one of the " + f"{len(targets)} registered targets.") + + +if __name__ == "__main__": + main() diff --git a/scripts/mutants-corpus-test.sh b/scripts/mutants-corpus-test.sh index b1409bae..663c8b6f 100755 --- a/scripts/mutants-corpus-test.sh +++ b/scripts/mutants-corpus-test.sh @@ -41,6 +41,7 @@ targets=( fuzz_privacy_preserving_witness fuzz_encoding_privacy_preserving fuzz_nullifier_set_roundtrip + fuzz_privacy_preserving_state_transition ) # cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.).