Skip to content

ci: add fuzz regression testing and continuous fuzzing infrastructure#7173

Open
thepastaclaw wants to merge 21 commits into
dashpay:developfrom
thepastaclaw:ci/fuzz-regression
Open

ci: add fuzz regression testing and continuous fuzzing infrastructure#7173
thepastaclaw wants to merge 21 commits into
dashpay:developfrom
thepastaclaw:ci/fuzz-regression

Conversation

@thepastaclaw
Copy link
Copy Markdown

@thepastaclaw thepastaclaw commented Feb 23, 2026

Summary

Consolidates all active Dash Core fuzzing work into a single branch/PR
so we avoid cross-PR merge conflicts.

This PR now contains both:

  1. Fuzz-target expansion for Dash-specific code paths
    (deserialization, roundtrip, and functional validators)
  2. Fuzzing infrastructure
    (CI regression workflow, corpus tooling, and continuous fuzz daemon)

This is the end-to-end delivery for the Dash Core fuzzing initiative
(tracker#108).

Included work (folded into this PR)

The following previously separate fuzz PRs were merged into this branch
and closed in favor of this consolidated PR:

  • #7161
    — Dash-specific deserialization fuzz targets
  • #7162
    — roundtrip serialize/deserialize fuzz targets
  • #7166
    — governance proposal validator fuzz target
  • #7167
    — BLS operations + BLS IES fuzz targets
  • #7168
    — asset lock/unlock validation fuzz targets
  • #7169
    — special transaction validation fuzz target
  • #7172
    — LLMQ message handler fuzz target
  • #7174
    — CoinJoin validation fuzz targets

What this PR now delivers

1) Dash-specific fuzz target coverage

Adds broad fuzz coverage for Dash-specific serialization and functional
validation paths across:

  • evo/ (ProTx, asset lock/unlock, deterministic MN structures)
  • llmq/ (commitments, DKG/session message handling, quorum data)
  • governance/ (objects/votes/proposal validation)
  • coinjoin/ (queue/broadcast/denomination/status/entry logic)
  • bls/ (operations, aggregation, threshold ops, IES encryption)

2) CI fuzz regression workflow (test-fuzz.yml)

  • Runs after existing linux64_fuzz
  • Replays corpus inputs against all fuzz targets
  • Seeds inherited targets from bitcoin-core/qa-assets
  • Generates synthetic seeds for Dash-specific targets
  • Runs smoke tests for targets without corpus
  • Controlled by existing SKIP_LINUX64_FUZZ variable

3) Continuous fuzzing daemon

contrib/fuzz/continuous_fuzz_daemon.sh:

  • Loops all targets with configurable per-target runtime
  • Persists corpus at ~/fuzz_corpus/<target>/
  • Stores crash artifacts at ~/fuzz_crashes/<target>/
  • Supports daemon mode and --single-cycle
  • Includes a systemd service file for deployment

4) Seed corpus generator

contrib/fuzz/seed_corpus_from_chain.py:

  • Extracts real serialized artifacts from a running Dash node
  • Covers blocks, special transactions, governance, MN/quorum data
  • Supports --synthetic-only for offline corpus generation

Validation

Build verification

Built fuzz binary with --enable-fuzz-binary (PROVIDE_FUZZ_MAIN_FUNCTION
deterministic mode) on macOS arm64 (Apple Silicon, clang, 16 cores).
Binary compiled successfully with all 95 Dash-specific targets registered
(252 total targets including inherited Bitcoin Core targets).

Target registration check

All 95 new Dash-specific fuzz targets confirmed registered in the binary:

  • 43 deserialization targets covering: evo (ProTx, asset lock/unlock,
    deterministic MN, credit pool), llmq (commitments, DKG messages, quorum
    snapshots, sig shares), governance (objects, votes), coinjoin (queue,
    broadcast, entry, status, accept), bls (IES encrypted blobs, multi-recipient)
  • 37 roundtrip targets verifying serialize-deserialize-reserialize
    consistency for all major Dash protocol structures
  • 15 functional targets: asset_lock_tx, asset_lock_tx_raw,
    asset_unlock_fee, bls_operations, bls_ies, coinjoin_broadcasttx,
    coinjoin_denominations, coinjoin_entry_addscriptsig, coinjoin_queue,
    coinjoin_status_update, deterministic_mn_list_diff,
    governance_proposal_validator, llmq_messages, special_tx_validation,
    process_message_dash

Concrete run results

Each of the 95 Dash-specific targets was exercised with 21 inputs
(20 random seed inputs of varying sizes 1-256 bytes + 1 empty input).
All targets processed every input without crashes or hangs.

Summary: 95/95 targets PASS, 0 crashes, 0 timeouts.

Category Targets Inputs each Result
Deserialization (evo) 13 21 all PASS
Deserialization (llmq) 14 21 all PASS
Deserialization (governance) 5 21 all PASS
Deserialization (coinjoin) 5 21 all PASS
Deserialization (bls) 3 21 all PASS
Deserialization (other) 3 21 all PASS
Roundtrip (all domains) 37 21 all PASS
Functional validators 15 21 all PASS

Total inputs processed: 1,995 (95 targets x 21 inputs)

Continuous daemon validation

Daemon dry-run confirmed on Guix VM — target enumeration, corpus dir
creation, and crash dir creation all verified:

./continuous_fuzz_daemon.sh --dry-run
./continuous_fuzz_daemon.sh --single-cycle --targets bls_operations \
  --time-per-target 10

Seed corpus generation

Synthetic corpus generation validated (offline mode, no running node):

python3 contrib/fuzz/seed_corpus_from_chain.py --synthetic-only -o /tmp/corpus

CI behavior

  • Reuses existing linux64_fuzz artifacts
  • Integrated as a regression gate for corpus replay on PRs

Notes on validation scope

This validation confirms all targets build, register, and handle arbitrary
input without crashing in deterministic mode. Extended fuzzing campaigns
(with libFuzzer engine + coverage feedback) are tracked separately under
the continuous fuzzing deployment plan.

Bugs surfaced during fuzzing (pre-existing issues)

  • CoinJoin timeout path UB surfaced by fuzzing
    (CCoinJoinQueue::IsTimeOutOfBounds signed overflow case)
  • BLS library leak surfaced during long bls_operations campaigns
    (libdashbls internals)
  • Roundtrip deserialize issue found/fixed in related work:
    CQuorumSnapshot::Unserialize missing mnSkipList.clear()
    before read loop

Notes

  • This PR supersedes the listed closed fuzz PRs as the single
    review/merge unit for fuzzing.
  • Keeping this work in one branch is intended to minimize rebase churn
    while review is in progress.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 23, 2026

✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds fuzzing infrastructure: a new reusable GitHub Actions job (test-linux64_fuzz) in .github/workflows/build.yml and a new workflow (.github/workflows/test-fuzz.yml) that downloads build artifacts, prepares corpora (inherited + synthetic), discovers fuzz targets in a fuzz binary, replays or exercises each target, collects crashes/logs, and reports a pass/fail summary (non-zero exit on failures). Adds contrib tools and docs: a continuous fuzzing daemon (contrib/fuzz/continuous_fuzz_daemon.sh), a corpus seeder that extracts real node data and creates synthetic seeds (contrib/fuzz/seed_corpus_from_chain.py), and documentation (contrib/fuzz/README.md).

Sequence Diagram(s)

sequenceDiagram
    participant GHA as GitHub Actions
    participant Artifact as Artifact Store
    participant Container as Fuzz Container
    participant FuzzBin as Fuzz Binary
    participant Reporter as Report Summary

    GHA->>Artifact: download build artifacts & seed corpus
    Artifact-->>GHA: deliver artifacts and corpora
    GHA->>Container: start container with inputs (bundle-key, build-target, container-path)
    Container->>FuzzBin: discover fuzz targets
    FuzzBin-->>Container: return target list
    Container->>Container: prepare corpus (synthetic + inherited)
    loop per fuzz target
        Container->>FuzzBin: run target (replay or empty-corpus run)
        FuzzBin-->>Container: execution result (pass/fail/crash)
        Container->>Container: collect artifacts, logs, crashes
    end
    Container->>Reporter: produce summary (Passed/Failed/Total)
    Reporter-->>GHA: return exit status / upload results
Loading
sequenceDiagram
    participant Seeder as seed_corpus_from_chain.py
    participant Node as Dash Node (RPC)
    participant CorpusDir as Corpus Directory

    Seeder->>Node: query recent blocks, txs, governance, masternode, quorum data
    Node-->>Seeder: return JSON/hex data
    Seeder->>CorpusDir: save serialized inputs (SHA256-named files) per target
    Seeder->>CorpusDir: generate synthetic seeds for multiple targets
    CorpusDir-->>Seeder: return per-target file counts and total size
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'ci: add fuzz regression testing and continuous fuzzing infrastructure' clearly and concisely summarizes the main changes: adding CI fuzz regression testing and continuous fuzzing infrastructure to the project.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description is directly related to the changeset, providing a comprehensive overview of the fuzzing infrastructure additions including CI workflows, daemon scripts, and corpus tools.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
.github/workflows/test-fuzz.yml (1)

127-138: No timeout guard on corpus replay — a hanging target can block the CI runner indefinitely.

The empty-corpus branch wraps the fuzz binary with timeout 30, but the corpus-replay branch (line 129) has no timeout. In libFuzzer, running a directory of test cases without fuzzing exits when all inputs are processed, but a target that hangs on a specific corpus input will block forever. Wrap the replay invocation with timeout (e.g., timeout $(($(find "$corpus_dir" -type f | wc -l) * 10 + 60)) or a fixed ceiling like 3600).

⏱️ Proposed fix
-            if FUZZ="$target" "$FUZZ_BIN" \
+            if FUZZ="$target" timeout 3600 "$FUZZ_BIN" \
                 -rss_limit_mb=4000 \
                 "$corpus_dir" 2>&1; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test-fuzz.yml around lines 127 - 138, The corpus-replay
block currently runs the fuzz binary with FUZZ="$target" "$FUZZ_BIN"
"$corpus_dir" without a timeout, so a hanging test can block CI; modify that
invocation to be wrapped with timeout (e.g., prefix with timeout 3600 or compute
timeout as $(($(find "$corpus_dir" -type f | wc -l) * 10 + 60))) so the command
using FUZZ="$target" "$FUZZ_BIN" "$corpus_dir" is subject to a bounded runtime,
and keep the existing success/failure handling (PASS/FAIL, PASSED/FAILED
counters) unchanged.
contrib/fuzz/continuous_fuzz_daemon.sh (1)

161-163: find with -o predicates should be parenthesized for clarity and safety.

Both crash-count expressions (-name 'crash-*' -o -name 'timeout-*' -o -name 'oom-*') work correctly as written since no other predicates are ANDed in, but without explicit \( ... \) grouping the intent is ambiguous and adding -type f later would silently break the logic.

🔧 Proposed fix
-    crash_count=$(find "$target_crashes" -name 'crash-*' -o -name 'timeout-*' -o -name 'oom-*' 2>/dev/null | wc -l)
+    crash_count=$(find "$target_crashes" \( -name 'crash-*' -o -name 'timeout-*' -o -name 'oom-*' \) 2>/dev/null | wc -l)

Apply the same fix to the total_crashes expression at line 233.

Also applies to: 231-233

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/fuzz/continuous_fuzz_daemon.sh` around lines 161 - 163, The find
invocations use multiple -name predicates combined with -o without explicit
grouping, which is ambiguous and brittle; update both the crash_count and
total_crashes find commands (the lines that set crash_count=$(find ... | wc -l)
and total_crashes=...) to wrap the name predicates in escaped parentheses \( ...
\) so the three -name tests are grouped (and if you later add -type f it won't
change the logic); ensure any -type f predicate is placed outside the grouped
parentheses or combined with -a as intended.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/test-fuzz.yml:
- Around line 93-95: The SKIPPED counter is never incremented but is initialized
and printed ("Skipped: $SKIPPED"), so either remove the unused SKIPPED variable
and its summary output or wire it up where targets are actually skipped;
specifically update the logic that checks for missing corpus directories or
other early-continue paths to increment SKIPPED (increment SKIPPED with
SKIPPED=$((SKIPPED+1)) before continuing) or delete both the SKIPPED=0
initialization and the "Skipped: $SKIPPED" summary line to avoid a misleading
always-zero metric.
- Line 25: Remove the unused environment variable
CI_FAILFAST_TEST_LEAVE_DANGLING from the workflow: locate the env entry
CI_FAILFAST_TEST_LEAVE_DANGLING in the job/env block and delete that key (and
its value) so the workflow no longer sets this unused variable.

In `@contrib/fuzz/seed_corpus_from_chain.py`:
- Around line 211-219: The code is saving JSON text as hex bytes for the
"dash_quorum_snapshot_deserialize" target (qinfo = dash_cli(...); qinfo_hex =
qinfo.encode().hex()), which is wrong for a binary-deserialize fuzz target;
instead, call an RPC that returns the serialized quorum snapshot (or obtain the
serialized blob from the JSON response if it contains a hex/base64 field) and
pass that raw hex-serialized snapshot into save_corpus_input for the
"dash_quorum_snapshot_deserialize" target; update the logic around dash_cli,
qinfo and qinfo_hex (and the branch that calls save_corpus_input) to use the
actual serialized snapshot bytes (hex string) or skip this target if a
serialized form isn't available.
- Around line 158-168: The loop over objects uses an unused loop variable named
obj_hash; rename it to _obj_hash to satisfy Ruff B007 and indicate it's
intentionally unused; update the for statement in the block that calls
json.loads(result) and iterates "for obj_hash, obj_data in objects.items()" to
"for _obj_hash, obj_data in objects.items()" while leaving the body that calls
save_corpus_input (for "dash_governance_object_deserialize" and
"dash_governance_object_roundtrip") unchanged; no other logic changes are
required.
- Line 22: Remove the unused import by deleting the line that imports the os
module in contrib/fuzz/seed_corpus_from_chain.py (the standalone "import os"
statement) to resolve the Flake8 F401 unused-import warning; ensure no other
references to os remain in functions or top-level code before removing.
- Around line 137-144: The current block saves the entire raw_tx for special
transactions to non-existent fuzz targets and doesn't extract the payload;
either remove this code path or implement it properly: when extra_payload > 0
(variable extra_payload) and tx_type in type_map, slice out the payload bytes as
payload = raw_tx[-extra_payload:] (instead of saving raw_tx), then save payload
via save_corpus_input using a target name that actually exists (update type_map
values to real fuzz target names or add the corresponding fuzz targets), and/or
guard the save loop (for suffix in ["_deserialize","_roundtrip"]) by verifying
the targets exist (or simply drop the loop and block if you choose to remove
incomplete support).

---

Nitpick comments:
In @.github/workflows/test-fuzz.yml:
- Around line 127-138: The corpus-replay block currently runs the fuzz binary
with FUZZ="$target" "$FUZZ_BIN" "$corpus_dir" without a timeout, so a hanging
test can block CI; modify that invocation to be wrapped with timeout (e.g.,
prefix with timeout 3600 or compute timeout as $(($(find "$corpus_dir" -type f |
wc -l) * 10 + 60))) so the command using FUZZ="$target" "$FUZZ_BIN"
"$corpus_dir" is subject to a bounded runtime, and keep the existing
success/failure handling (PASS/FAIL, PASSED/FAILED counters) unchanged.

In `@contrib/fuzz/continuous_fuzz_daemon.sh`:
- Around line 161-163: The find invocations use multiple -name predicates
combined with -o without explicit grouping, which is ambiguous and brittle;
update both the crash_count and total_crashes find commands (the lines that set
crash_count=$(find ... | wc -l) and total_crashes=...) to wrap the name
predicates in escaped parentheses \( ... \) so the three -name tests are grouped
(and if you later add -type f it won't change the logic); ensure any -type f
predicate is placed outside the grouped parentheses or combined with -a as
intended.

Comment thread .github/workflows/test-fuzz.yml Outdated
Comment thread .github/workflows/test-fuzz.yml Outdated
Comment thread contrib/fuzz/seed_corpus_from_chain.py
Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
Comment thread contrib/fuzz/seed_corpus_from_chain.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contrib/fuzz/seed_corpus_from_chain.py`:
- Line 41: The function save_corpus_input currently declares an unused parameter
label; remove the unused parameter from the function signature (change def
save_corpus_input(output_dir, target_name, data_hex):) and update any local
calls within this module that pass a label to instead pass only output_dir,
target_name, data_hex, or if callers elsewhere need labeling, alternatively wire
label into a debug/log/print inside save_corpus_input (e.g., include label in a
logging statement) so the parameter is actually used; focus your change on the
save_corpus_input definition and any direct callers to keep signatures
consistent.
- Line 46: The code calls bytes.fromhex(data_hex) which can raise ValueError for
malformed/non-hex RPC output; wrap this conversion in a try/except ValueError
around the bytes.fromhex(data_hex) call (the expression that assigns raw_bytes)
and handle failures by skipping the bad entry (e.g., continue) and optionally
logging the offending data_hex so the extractor doesn’t abort the whole
run—ensure you reference the same symbols (data_hex and raw_bytes) when adding
the try/except and any log message.

---

Duplicate comments:
In `@contrib/fuzz/seed_corpus_from_chain.py`:
- Around line 136-143: The loop is saving the full raw_tx to fuzz targets that
don't exist and ignores the comment that the payload is the last extra_payload
bytes; update the block in seed_corpus_from_chain.py to slice out only the last
extra_payload bytes (e.g., payload = raw_tx[-extra_payload:]) and pass that
payload to save_corpus_input instead of raw_tx, and also ensure the constructed
target names (the values in type_map combined with suffixes like "_deserialize"
and "_roundtrip") match actual registered fuzz target names in src/test/fuzz (or
adjust type_map to the correct target names) so save_corpus_input(output_dir,
f"{target}{suffix}", payload) writes to real targets.
- Around line 188-193: extract_masternode_list is saving the full
getrawtransaction bytes into non-existent/incorrect fuzz targets
(dash_proreg_tx_deserialize, dash_proreg_tx_roundtrip,
dash_deterministic_mn_deserialize); change the logic around
raw_tx/dash_cli/save_corpus_input so you extract the ProReg payload bytes (the
isolated ProReg payload parsed from the transaction) instead of the full
serialized transaction, and save those payload bytes to the correct existing
fuzz target names used under src/test/fuzz (match the target names used by
extract_special_txs); locate the raw_tx assignment and the loop over targets and
replace the raw_tx writes with the extracted proreg_payload variable (or re-use
the extraction routine from extract_special_txs) and ensure the target strings
passed to save_corpus_input correspond to real fuzzers.

Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
@thepastaclaw
Copy link
Copy Markdown
Author

thepastaclaw commented Feb 23, 2026

Follow-up fix pushed in commit 5cac8c1:

  • Added export LC_ALL=C to contrib/fuzz/continuous_fuzz_daemon.sh to satisfy lint-shell-locale.py.
  • Local validation run on branch:
    • python3 test/lint/lint-shell-locale.py
    • python3 test/lint/lint-shell.py

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contrib/fuzz/continuous_fuzz_daemon.sh`:
- Around line 171-173: The pipeline using grep against "$target_log" can return
exit status 1 when no matches are found which, combined with set -o pipefail and
set -e, will abort the daemon; modify the crash-logging pipeline (the grep ... |
while IFS= read -r line; do log "CRASH" "  $line"; done) to prevent a non-zero
exit from grep from failing the script — for example wrap that pipeline so
failures are ignored (append a harmless "|| true" to the pipeline) or
temporarily disable pipefail around this block, ensuring you still reference the
same variables and the log function and keep the existing crash_count guard.
- Around line 150-151: In continuous_fuzz_daemon.sh the inline environment
assignment for ASAN_OPTIONS overwrites any caller-provided options; change it to
preserve existing ASAN_OPTIONS and append (or prepend) the required option
(detect_leaks=0) using the standard colon-separated form so caller values like
abort_on_error=1 or symbolize=1 are retained, while still setting FUZZ="$target"
as before; update the assignment where ASAN_OPTIONS is set to merge with the
existing ASAN_OPTIONS rather than replacing it.

Comment thread contrib/fuzz/continuous_fuzz_daemon.sh Outdated
Comment thread contrib/fuzz/continuous_fuzz_daemon.sh Outdated
@UdjinM6
Copy link
Copy Markdown

UdjinM6 commented Feb 23, 2026

Issues and Suggestions

High Priority

  1. test-fuzz.yml:83 — git clone failure silently ignored
    git clone --depth=1 https://github.com/bitcoin-core/qa-assets /tmp/qa-assets || true
    If the clone fails (network issue), the entire regression test runs with an empty corpus, and only the 10-second empty-corpus mode is exercised. This makes the CI step pass vacuously. Consider at minimum logging a warning, or better, failing if the clone fails (since without corpus data the regression test is mostly meaningless).

  2. test-fuzz.yml:127-128 — Missing corpus runs for 10 seconds but timeout is 30 seconds

if FUZZ="$target" timeout 30 "$FUZZ_BIN" \
    -max_total_time=10 \

The 30s timeout wrapper around a 10s max_total_time is redundant but benign. However, libFuzzer may not always honor max_total_time if stuck in initialization — the external timeout is a good safety net. Consider aligning them or adding a comment explaining the intentional gap.

  1. seed_corpus_from_chain.py:678-683 — Incorrect payload extraction logic
extra_payload = tx.get("extraPayloadSize", 0)
if extra_payload > 0 and tx_type in type_map:
    target = type_map[tx_type]
    for suffix in ["_deserialize", "_roundtrip"]:
        if save_corpus_input(output_dir, f"{target}{suffix}", raw_tx):

The comment says "The extra payload is the last N bytes of the raw tx", but the code saves the entire raw transaction (raw_tx), not just the payload bytes. If the fuzz targets expect only the payload (as the target names like dash_proreg_tx_deserialize suggest), this gives them wrong input. If they expect the full transaction, that's fine — but the comment is misleading. Clarify the intent or actually extract just the payload bytes.

  1. continuous_fuzz_daemon.sh:370 — nproc not portable (macOS)
    JOBS=$(( $(nproc) / 2 ))
    Since this daemon is meant for developer use (not just CI), it should use the portable version, consider $(nproc 2>/dev/null || sysctl -n hw.ncpu).

Medium Priority

  1. test-fuzz.yml:111 — detect_leaks=0 in ASAN_OPTIONS
    Leak detection is disabled. This is likely intentional (leak detection in fuzz targets is often noisy), but worth a brief comment explaining why.

  2. continuous_fuzz_daemon.sh:439 — ASAN_OPTIONS only sets detect_leaks=0
    The CI workflow sets much more comprehensive sanitizer options (detect_stack_use_after_return, check_initialization_order, etc.). The daemon should either match these or document why it uses reduced options.

  3. continuous_fuzz_daemon.sh:509 — shuf is not available on macOS by default
    shuffled=$(echo "$targets" | shuf)
    Consider a fallback: shuf is GNU coreutils, not available on macOS without brew install coreutils. Could use sort -R or awk as a fallback.

  4. test-fuzz.yml:154 — ls "$corpus_dir" | wc -l counts may be misleading
    echo "::group::${target} ($(ls "$corpus_dir" | wc -l) inputs)"
    Hidden files (unlikely but possible) wouldn't be counted; use find "$corpus_dir" -maxdepth 1 -type f | wc -l for accuracy.

@thepastaclaw
Copy link
Copy Markdown
Author

Thanks for the detailed review — I pushed follow-up fixes in ef6525050e and addressed each item:

  1. test-fuzz.yml clone failure ignored

    • qa-assets clone now fails loudly and exits non-zero instead of || true.
  2. 10s run under 30s timeout

    • Kept the external timeout as a safety net and added an explicit inline comment explaining the intentional gap (timeout 30 vs -max_total_time=10) for startup hangs.
  3. seed_corpus_from_chain.py payload extraction mismatch

    • Updated to extract and store only extraPayload bytes using extraPayloadSize + CompactSize prefix validation.
    • If extraction is invalid/mismatched, it now warns and skips instead of saving incorrect data.
  4. nproc portability in daemon

    • Added portable CPU detection: nprocsysctl -n hw.ncpu → fallback 2.
  5. detect_leaks=0 rationale in workflow

    • Added inline comment explaining this is intentional for fuzz regression due to libFuzzer/LSan noise.
  6. Daemon ASAN options narrower than CI

    • Expanded daemon ASAN_OPTIONS to align with CI defaults:
      • detect_stack_use_after_return=1
      • check_initialization_order=1
      • strict_init_order=1
      • detect_leaks=0
  7. shuf portability in daemon

    • Added fallback shuffle path when shuf is unavailable (awk+sort randomization).
  8. ls | wc -l corpus count

    • Replaced with find "$corpus_dir" -maxdepth 1 -type f | wc -l.

Validation run locally:

  • bash -n contrib/fuzz/continuous_fuzz_daemon.sh
  • python3 -m py_compile contrib/fuzz/seed_corpus_from_chain.py

Commit: thepastaclaw@ef6525050e

@thepastaclaw
Copy link
Copy Markdown
Author

@UdjinM6 thanks for the detailed review — I addressed all 8 items in 3700fcc5562154f5f3b26d5af0cbffcfcb6b9968.

Point-by-point:

  1. test-fuzz.yml: silent git clone ... || true
    ✅ Fixed. Clone now hard-fails with explicit error output if qa-assets cannot be fetched.

  2. timeout 30 vs -max_total_time=10 gap
    ✅ Kept the safety net behavior and added an explicit comment documenting why the external timeout is intentionally larger.

  3. seed_corpus_from_chain.py: payload extraction mismatch
    ✅ Fixed by implementing real extra-payload extraction from raw special transactions (parse version/type, vin/vout CompactSize sections, locktime, payload CompactSize + payload bytes). The script now writes payload-only bytes into payload fuzz corpora.

  4. continuous_fuzz_daemon.sh: nproc portability
    ✅ Fixed with portable CPU detection fallback chain: nprocsysctl -n hw.ncpugetconf _NPROCESSORS_ONLN1.

  5. test-fuzz.yml: detect_leaks=0 rationale
    ✅ Added inline comment explaining intentional leak-detection disablement for fuzz regression stability/noise reasons.

  6. continuous_fuzz_daemon.sh: reduced ASAN options
    ✅ Updated daemon ASAN_OPTIONS to CI-aligned settings (detect_stack_use_after_return, check_initialization_order, strict_init_order, detect_leaks=0).

  7. continuous_fuzz_daemon.sh: shuf missing on macOS
    ✅ Fixed by adding shuffle_lines() with shuf when available and an awk|sort fallback otherwise.

  8. test-fuzz.yml: ls | wc -l corpus counts
    ✅ Replaced with find "$corpus_dir" -maxdepth 1 -type f | wc -l.

Validation run locally:

  • bash -n contrib/fuzz/continuous_fuzz_daemon.sh
  • python3 -m py_compile contrib/fuzz/seed_corpus_from_chain.py

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
contrib/fuzz/seed_corpus_from_chain.py (1)

41-41: ⚠️ Potential issue | 🟡 Minor

Unused label parameter still present (Ruff ARG001).

The label="" parameter is declared but never referenced in the function body. Ruff still fires ARG001 on this line.

🔧 Proposed fix (drop the parameter)
-def save_corpus_input(output_dir, target_name, data_hex, label=""):
+def save_corpus_input(output_dir, target_name, data_hex):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/fuzz/seed_corpus_from_chain.py` at line 41, The function
save_corpus_input declares an unused parameter label="" which triggers Ruff
ARG001; remove the label parameter from the function signature in
save_corpus_input and update any callers that pass a label argument to stop
supplying it (or adjust them to only pass output_dir, target_name, data_hex),
and run tests/lint to ensure no remaining references to label remain.
.github/workflows/test-fuzz.yml (1)

25-25: ⚠️ Potential issue | 🟡 Minor

CI_FAILFAST_TEST_LEAVE_DANGLING is set but never referenced.

This env var is defined at the workflow level but is not consumed anywhere in this workflow or in any script it calls. It can be safely removed.

🔧 Proposed fix
-env:
-  CI_FAILFAST_TEST_LEAVE_DANGLING: 1
-
 jobs:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test-fuzz.yml at line 25, Remove the unused workflow
environment variable CI_FAILFAST_TEST_LEAVE_DANGLING from the workflow
definition: locate the environment block where CI_FAILFAST_TEST_LEAVE_DANGLING
is set and delete that key (the variable name CI_FAILFAST_TEST_LEAVE_DANGLING)
so the workflow no longer defines an unused env var; before removing, grep for
CI_FAILFAST_TEST_LEAVE_DANGLING across the repo to confirm nothing references it
and then commit the simplified workflow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/test-fuzz.yml:
- Around line 93-95: The TARGETS enumeration swallows real startup failures due
to the "|| true" and yields an empty string which makes TARGET_COUNT incorrectly
report 1; after populating TARGETS (from PRINT_ALL_FUZZ_TARGETS_AND_ABORT via
FUZZ_BIN) add an explicit guard that checks whether TARGETS is empty or contains
an error marker and aborts with a non-zero exit and an explanatory error log if
so; use the variables TARGETS, TARGET_COUNT, and FUZZ_BIN in this check (e.g.,
test for empty string or for common loader/architecture errors in TARGETS) so
the workflow fails fast on genuine startup problems rather than reporting a
vacuous pass.

In `@contrib/fuzz/seed_corpus_from_chain.py`:
- Around line 244-250: The extract_masternode_list function is saving the full
raw ProRegTx to targets that expect only the extra payload bytes; change it to
call extract_extra_payload_hex(raw_tx) and pass that payload hex to
save_corpus_input for the dash_proreg_tx_deserialize and
dash_proreg_tx_roundtrip targets (mirroring extract_special_txs), and do not
feed the full transaction there. For dash_deterministic_mn_deserialize,
determine the expected serialized CDeterministicMN format and extract/convert
the appropriate bytes from the ProRegTx payload (or use an existing helper that
converts a ProRegTx payload into a CDeterministicMN serialization) and pass that
serialized data to save_corpus_input instead of the full raw_tx; update
extract_masternode_list to reference extract_extra_payload_hex,
save_corpus_input, extract_masternode_list, and
dash_deterministic_mn_deserialize so the correct payload bytes are saved for
each target.

---

Duplicate comments:
In @.github/workflows/test-fuzz.yml:
- Line 25: Remove the unused workflow environment variable
CI_FAILFAST_TEST_LEAVE_DANGLING from the workflow definition: locate the
environment block where CI_FAILFAST_TEST_LEAVE_DANGLING is set and delete that
key (the variable name CI_FAILFAST_TEST_LEAVE_DANGLING) so the workflow no
longer defines an unused env var; before removing, grep for
CI_FAILFAST_TEST_LEAVE_DANGLING across the repo to confirm nothing references it
and then commit the simplified workflow.

In `@contrib/fuzz/seed_corpus_from_chain.py`:
- Line 41: The function save_corpus_input declares an unused parameter label=""
which triggers Ruff ARG001; remove the label parameter from the function
signature in save_corpus_input and update any callers that pass a label argument
to stop supplying it (or adjust them to only pass output_dir, target_name,
data_hex), and run tests/lint to ensure no remaining references to label remain.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cac8c1 and ef65250.

📒 Files selected for processing (3)
  • .github/workflows/test-fuzz.yml
  • contrib/fuzz/continuous_fuzz_daemon.sh
  • contrib/fuzz/seed_corpus_from_chain.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • contrib/fuzz/continuous_fuzz_daemon.sh

Comment thread .github/workflows/test-fuzz.yml Outdated
Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
.github/workflows/test-fuzz.yml (2)

25-25: ⚠️ Potential issue | 🟡 Minor

Unused environment variable CI_FAILFAST_TEST_LEAVE_DANGLING.

This variable is set but never referenced anywhere in the workflow or any called scripts.

🔧 Proposed fix
-env:
-  CI_FAILFAST_TEST_LEAVE_DANGLING: 1
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test-fuzz.yml at line 25, The environment variable
CI_FAILFAST_TEST_LEAVE_DANGLING is defined in the workflow but never used;
either remove the CI_FAILFAST_TEST_LEAVE_DANGLING entry from the workflow file
or wire it into the code path that needs it (e.g., pass it into steps as env or
reference it in called scripts/commands), ensuring references use the exact name
CI_FAILFAST_TEST_LEAVE_DANGLING so there are no unused vars left in the
workflow.

93-95: ⚠️ Potential issue | 🟡 Minor

Binary startup failure produces a vacuous pass.

|| true suppresses genuine startup failures (missing shared libraries, wrong architecture, etc.). In that case TARGETS is empty, TARGET_COUNT becomes 1 (off-by-one from echo "" | wc -l), the loop skips the single empty line, and the step exits 0 — reporting success with zero targets tested.

🛡️ Proposed fix
  TARGETS=$(PRINT_ALL_FUZZ_TARGETS_AND_ABORT=1 "$FUZZ_BIN" 2>&1 || true)
+ if [ -z "$TARGETS" ]; then
+   echo "ERROR: No fuzz targets found; fuzz binary may not have started correctly"
+   exit 1
+ fi
  TARGET_COUNT=$(echo "$TARGETS" | wc -l)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test-fuzz.yml around lines 93 - 95, The current line uses
"TARGETS=$(PRINT_ALL_FUZZ_TARGETS_AND_ABORT=1 "$FUZZ_BIN" 2>&1 || true)" which
masks startup failures and causes an empty output to be treated as a successful
run; remove the "|| true" and instead check the command exit status and fail the
step on non-zero to surface missing libraries/arch issues. Also compute
TARGET_COUNT by counting non-empty lines in TARGETS (e.g., count lines that are
not blank) so an empty output yields 0 targets rather than 1; refer to the
variables/flags TARGETS, TARGET_COUNT, PRINT_ALL_FUZZ_TARGETS_AND_ABORT and the
executable "$FUZZ_BIN" when making these changes.
contrib/fuzz/continuous_fuzz_daemon.sh (2)

174-184: ⚠️ Potential issue | 🟡 Minor

ASAN_OPTIONS still overwrites any caller-supplied value.

The inline assignment always sets a fixed string; users running with custom options like abort_on_error=1 or symbolize=1 will silently lose them.

🛡️ Proposed fix
-    ASAN_OPTIONS="detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0" \
+    ASAN_OPTIONS="${ASAN_OPTIONS:+${ASAN_OPTIONS}:}detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0" \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/fuzz/continuous_fuzz_daemon.sh` around lines 174 - 184, The inline
assignment for ASAN_OPTIONS overwrites any caller-supplied value; change the
invocation around "$FUZZ_BIN" so it preserves existing ASAN_OPTIONS by
concatenating them with the defaults instead of replacing them (e.g. build
ASAN_OPTIONS using the existing ASAN_OPTIONS variable if set, then append the
defaults like
detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0),
and use that combined variable in the inline env assignment before invoking
"$FUZZ_BIN" so options such as abort_on_error or symbolize provided by callers
are retained.

195-197: ⚠️ Potential issue | 🟠 Major

set -o pipefail + zero grep matches aborts the daemon.

When the log contains no matching lines, grep exits with status 1. With set -euo pipefail, the pipeline grep ... | while IFS= read -r line; ... propagates that status 1, which set -e then turns into a daemon abort — even though the intent is simply "skip if nothing matches".

🐛 Proposed fix
-        grep -E "SUMMARY|ERROR|BINGO|crash-|timeout-|oom-" "$target_log" 2>/dev/null | while IFS= read -r line; do
-            log "CRASH" "  $line"
-        done
+        grep -E "SUMMARY|ERROR|BINGO|crash-|timeout-|oom-" "$target_log" 2>/dev/null | while IFS= read -r line; do
+            log "CRASH" "  $line"
+        done || true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/fuzz/continuous_fuzz_daemon.sh` around lines 195 - 197, The pipeline
using grep on "$target_log" can return exit status 1 when there are no matches,
which with set -euo pipefail aborts the daemon; fix by testing for matches first
and only running the pipeline when matches exist: use grep -Eq
"SUMMARY|ERROR|BINGO|crash-|timeout-|oom-" "$target_log" 2>/dev/null && grep -E
"SUMMARY|ERROR|BINGO|crash-|timeout-|oom-" "$target_log" 2>/dev/null | while
IFS= read -r line; do log "CRASH" "  $line"; done, so the while loop only runs
when grep finds matches (references: "$target_log" and the grep ... | while IFS=
read -r line; do log "CRASH" ... done pipeline).
contrib/fuzz/seed_corpus_from_chain.py (2)

41-41: ⚠️ Potential issue | 🟡 Minor

Unused label parameter in save_corpus_input.

label="" is declared but never referenced in the function body. Either use it (e.g., include it in a debug print) or drop it from the signature and all call sites.

🔧 Proposed fix (drop the parameter)
-def save_corpus_input(output_dir, target_name, data_hex, label=""):
+def save_corpus_input(output_dir, target_name, data_hex):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/fuzz/seed_corpus_from_chain.py` at line 41, The function
save_corpus_input declares an unused parameter label="" which should be removed
to avoid dead API surface; update the function signature in save_corpus_input to
drop the label parameter and remove it from all call sites (or alternatively, if
label is intended, thread it into the filename/logging within save_corpus_input
such as appending to the output filename or including in debug output) so there
are no unused parameters left; ensure references to save_corpus_input elsewhere
match the new signature (or use the label inside the function if you choose that
route).

274-299: ⚠️ Potential issue | 🟠 Major

extract_masternode_list saves the full raw transaction to payload-specific targets.

extract_special_txs correctly calls extract_extra_payload_hex() to strip the extra-payload bytes before saving to dash_proreg_tx_deserialize / dash_proreg_tx_roundtrip. extract_masternode_list bypasses that extraction and writes the entire raw transaction to the same targets, producing structurally mismatched corpus inputs that will generate only spurious parse errors at fuzz time. dash_deterministic_mn_deserialize almost certainly expects CDeterministicMN bytes, not a full ProRegTx.

🐛 Proposed fix
                 raw_tx = dash_cli("getrawtransaction", protx_hash, datadir=datadir)
                 if raw_tx:
-                    for target in ["dash_proreg_tx_deserialize", "dash_proreg_tx_roundtrip",
-                                   "dash_deterministic_mn_deserialize"]:
-                        if save_corpus_input(output_dir, target, raw_tx):
-                            saved += 1
+                    # Extract payload for proreg payload-specific targets
+                    # Fetch full tx info to get extraPayloadSize
+                    tx_info_str = dash_cli("getrawtransaction", protx_hash, "1", datadir=datadir)
+                    extra_payload_size = 0
+                    if tx_info_str:
+                        try:
+                            tx_info = json.loads(tx_info_str)
+                            extra_payload_size = int(tx_info.get("extraPayloadSize", 0))
+                        except (json.JSONDecodeError, TypeError, ValueError):
+                            pass
+                    proreg_payload_hex, err = extract_extra_payload_hex(raw_tx, extra_payload_size)
+                    if proreg_payload_hex:
+                        for target in ["dash_proreg_tx_deserialize", "dash_proreg_tx_roundtrip"]:
+                            if save_corpus_input(output_dir, target, proreg_payload_hex):
+                                saved += 1
+                    # NOTE: dash_deterministic_mn_deserialize expects CDeterministicMN bytes;
+                    # verify the correct binary format and implement extraction separately.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/fuzz/seed_corpus_from_chain.py` around lines 274 - 299, The function
extract_masternode_list currently writes the entire raw transaction to
payload-specific fuzz targets, causing malformed inputs; instead, call the
existing extract_extra_payload_hex(raw_tx) and save its returned extra-payload
hex to the dash_proreg_tx_deserialize and dash_proreg_tx_roundtrip targets, and
do NOT save the full raw_tx there; for dash_deterministic_mn_deserialize, do not
save the full tx either—either extract the CDeterministicMN bytes (using the
same extractor if it returns the deterministic-MN blob) or skip saving to that
target until a proper extractor exists, and update the loop in
extract_masternode_list to use the extracted payload variable for the
appropriate targets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/test-fuzz.yml:
- Around line 134-136: The corpus replay invocation using FUZZ="$target"
"$FUZZ_BIN" -rss_limit_mb=4000 "$corpus_dir" can run indefinitely because it
lacks a termination flag; modify the call that constructs the fuzzer command
(the line invoking FUZZ, "$FUZZ_BIN", and "$corpus_dir") to include a
termination option such as -runs=0 (or alternatively -max_total_time=<seconds>)
so libFuzzer will only replay the corpus and exit instead of entering unlimited
mutation mode.

---

Duplicate comments:
In @.github/workflows/test-fuzz.yml:
- Line 25: The environment variable CI_FAILFAST_TEST_LEAVE_DANGLING is defined
in the workflow but never used; either remove the
CI_FAILFAST_TEST_LEAVE_DANGLING entry from the workflow file or wire it into the
code path that needs it (e.g., pass it into steps as env or reference it in
called scripts/commands), ensuring references use the exact name
CI_FAILFAST_TEST_LEAVE_DANGLING so there are no unused vars left in the
workflow.
- Around line 93-95: The current line uses
"TARGETS=$(PRINT_ALL_FUZZ_TARGETS_AND_ABORT=1 "$FUZZ_BIN" 2>&1 || true)" which
masks startup failures and causes an empty output to be treated as a successful
run; remove the "|| true" and instead check the command exit status and fail the
step on non-zero to surface missing libraries/arch issues. Also compute
TARGET_COUNT by counting non-empty lines in TARGETS (e.g., count lines that are
not blank) so an empty output yields 0 targets rather than 1; refer to the
variables/flags TARGETS, TARGET_COUNT, PRINT_ALL_FUZZ_TARGETS_AND_ABORT and the
executable "$FUZZ_BIN" when making these changes.

In `@contrib/fuzz/continuous_fuzz_daemon.sh`:
- Around line 174-184: The inline assignment for ASAN_OPTIONS overwrites any
caller-supplied value; change the invocation around "$FUZZ_BIN" so it preserves
existing ASAN_OPTIONS by concatenating them with the defaults instead of
replacing them (e.g. build ASAN_OPTIONS using the existing ASAN_OPTIONS variable
if set, then append the defaults like
detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0),
and use that combined variable in the inline env assignment before invoking
"$FUZZ_BIN" so options such as abort_on_error or symbolize provided by callers
are retained.
- Around line 195-197: The pipeline using grep on "$target_log" can return exit
status 1 when there are no matches, which with set -euo pipefail aborts the
daemon; fix by testing for matches first and only running the pipeline when
matches exist: use grep -Eq "SUMMARY|ERROR|BINGO|crash-|timeout-|oom-"
"$target_log" 2>/dev/null && grep -E "SUMMARY|ERROR|BINGO|crash-|timeout-|oom-"
"$target_log" 2>/dev/null | while IFS= read -r line; do log "CRASH" "  $line";
done, so the while loop only runs when grep finds matches (references:
"$target_log" and the grep ... | while IFS= read -r line; do log "CRASH" ...
done pipeline).

In `@contrib/fuzz/seed_corpus_from_chain.py`:
- Line 41: The function save_corpus_input declares an unused parameter label=""
which should be removed to avoid dead API surface; update the function signature
in save_corpus_input to drop the label parameter and remove it from all call
sites (or alternatively, if label is intended, thread it into the
filename/logging within save_corpus_input such as appending to the output
filename or including in debug output) so there are no unused parameters left;
ensure references to save_corpus_input elsewhere match the new signature (or use
the label inside the function if you choose that route).
- Around line 274-299: The function extract_masternode_list currently writes the
entire raw transaction to payload-specific fuzz targets, causing malformed
inputs; instead, call the existing extract_extra_payload_hex(raw_tx) and save
its returned extra-payload hex to the dash_proreg_tx_deserialize and
dash_proreg_tx_roundtrip targets, and do NOT save the full raw_tx there; for
dash_deterministic_mn_deserialize, do not save the full tx either—either extract
the CDeterministicMN bytes (using the same extractor if it returns the
deterministic-MN blob) or skip saving to that target until a proper extractor
exists, and update the loop in extract_masternode_list to use the extracted
payload variable for the appropriate targets.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef65250 and 3700fcc.

📒 Files selected for processing (3)
  • .github/workflows/test-fuzz.yml
  • contrib/fuzz/continuous_fuzz_daemon.sh
  • contrib/fuzz/seed_corpus_from_chain.py

Comment thread .github/workflows/test-fuzz.yml Outdated
@thepastaclaw
Copy link
Copy Markdown
Author

Copy link
Copy Markdown

@UdjinM6 UdjinM6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls squash fixes into original commits and rebase to get rid of "Merge remote...."

Comment thread .github/workflows/test-fuzz.yml
Comment thread .github/workflows/test-fuzz.yml Outdated
Comment thread src/Makefile.test.include Outdated
@thepastaclaw
Copy link
Copy Markdown
Author

Good catch — you're right. I’m cleaning this up now by squashing the fixup commits into their originals and rebasing to remove the merge commits (including the "Merge remote..." ones). I’ll push a cleaned history shortly.

thepastaclaw added a commit to thepastaclaw/dash that referenced this pull request Feb 24, 2026
Consolidate fuzz regression workflow, continuous fuzz daemon tooling, and Dash-specific fuzz target additions into a clean commit history for PR dashpay#7173 cleanup.
@UdjinM6
Copy link
Copy Markdown

UdjinM6 commented Feb 24, 2026

Good catch — you're right. I’m cleaning this up now by squashing the fixup commits into their originals and rebasing to remove the merge commits (including the "Merge remote..." ones). I’ll push a cleaned history shortly.

Btw, you squashed everything into one commit instead of squashing the fixup commits into their originals.

@thepastaclaw thepastaclaw marked this pull request as draft February 25, 2026 08:22
thepastaclaw added a commit to thepastaclaw/dash that referenced this pull request Feb 25, 2026
Consolidate fuzz regression workflow, continuous fuzz daemon tooling, and Dash-specific fuzz target additions into a clean commit history for PR dashpay#7173 cleanup.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93ff82d3f7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
Copy link
Copy Markdown
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

I re-validated the prior and current findings against the checked-out head 93ff82d3f7e2689df745623b041ebdcaaacd6c15. Two blocking issues still apply on this head: the fuzz regression workflow still runs open-ended libFuzzer sessions instead of a deterministic corpus replay, and createwallet still rejects explicit legacy-wallet requests when descriptors=false is passed without load_on_startup. The earlier governance corpus-seeding issue is resolved on this head because the generator now serializes full Governance::Object instances instead of writing raw DataHex payloads.

Reviewed commit: 93ff82d

🔴 2 blocking

1 additional finding

🔴 blocking: `createwallet` rejects explicit legacy-wallet calls whenever `descriptors` is set without `load_on_startup`

src/wallet/rpc/wallet.cpp (lines 679-682)

The new guard throws as soon as parameter 5 is present and parameter 6 is omitted, before checking whether descriptors was actually set to true. As a result, createwallet ... descriptors=false now fails even though the RPC help still documents descriptors=false as the way to request a legacy wallet and that call is otherwise unambiguous. The functional test framework masks this by supplying load_on_startup, but direct RPC and CLI callers still hit an unnecessary API break.

💡 Suggested change
if (!request.params[5].isNull() && request.params[5].get_bool() && request.params[6].isNull()) {
    throw JSONRPCError(RPC_INVALID_PARAMETER, "The createwallet RPC requires specifying the 'load_on_startup' flag when creating descriptor wallets. Dash Core v21 introduced this requirement due to breaking changes in the createwallet RPC.");
}
if (request.params[5].isNull() || request.params[5].get_bool()) {
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `.github/workflows/test-fuzz.yml`:
- [BLOCKING] lines 184-190: Fuzz regression step still starts unbounded fuzzing instead of replaying the saved corpus once
  This block is labeled as a corpus regression replay, but it invokes libFuzzer with the corpus directory and `-runs=0`. In libFuzzer that means there is no execution cap, so after replaying the existing inputs it immediately switches into normal mutation fuzzing until the outer `timeout 3600` kills it. That makes this CI job nondeterministic and much slower than intended, and it stops serving as a reliable regression check tied to the committed corpus.

In `src/wallet/rpc/wallet.cpp`:
- [BLOCKING] lines 679-682: `createwallet` rejects explicit legacy-wallet calls whenever `descriptors` is set without `load_on_startup`
  The new guard throws as soon as parameter 5 is present and parameter 6 is omitted, before checking whether `descriptors` was actually set to `true`. As a result, `createwallet ... descriptors=false` now fails even though the RPC help still documents `descriptors=false` as the way to request a legacy wallet and that call is otherwise unambiguous. The functional test framework masks this by supplying `load_on_startup`, but direct RPC and CLI callers still hit an unnecessary API break.

Comment thread .github/workflows/test-fuzz.yml
Copy link
Copy Markdown
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

PR adds substantial Dash-specific fuzz target coverage and a new regression-replay CI workflow. The fuzz harnesses are well-structured and production-code changes are minimal. Several legitimate suggestions remain: the regression workflow disables LSan globally (defeating the purpose of the suppressions file present alongside it), new Dash-only fuzz sources are not registered in non-backported.txt (excluding them from Dash-specific cppcheck), and several newly registered targets receive no seed coverage. Codex's blocking claim that -runs=0 does not perform corpus replay is incorrect — libFuzzer replays seed corpora unconditionally before checking MaxNumberOfRuns, so -runs=0 exits immediately after replay (still worth aligning with the project's existing -runs=1 convention).

Reviewed commit: 93ff82d

🟡 5 suggestion(s) | 💬 3 nitpick(s)

1 additional finding

🟡 suggestion: New Dash-specific fuzz sources are missing from non-backported.txt

test/util/data/non-backported.txt (lines 56-74)

This PR adds many Dash-only fuzz sources (asset_lock_unlock.cpp, bls_operations.cpp, coinjoin.cpp, deserialize_dash.cpp, deterministic_mn_list_diff.cpp, governance_proposal_validator.cpp, llmq_messages.cpp, process_message_dash.cpp, roundtrip_dash.cpp, simplified_mn_list_diff.cpp, special_tx_validation.cpp) under src/test/fuzz/, but test/util/data/non-backported.txt has no pattern matching them. test/lint/lint-cppcheck-dash.py (line 69) builds its file list exclusively from this allowlist, so the new harnesses are silently excluded from the Dash-specific cppcheck pass. Add an src/test/fuzz/<file>.cpp entry per new file (or a glob if appropriate) so the lint covers these new entry points.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `test/util/data/non-backported.txt`:
- [SUGGESTION] lines 56-74: New Dash-specific fuzz sources are missing from non-backported.txt
  This PR adds many Dash-only fuzz sources (`asset_lock_unlock.cpp`, `bls_operations.cpp`, `coinjoin.cpp`, `deserialize_dash.cpp`, `deterministic_mn_list_diff.cpp`, `governance_proposal_validator.cpp`, `llmq_messages.cpp`, `process_message_dash.cpp`, `roundtrip_dash.cpp`, `simplified_mn_list_diff.cpp`, `special_tx_validation.cpp`) under `src/test/fuzz/`, but `test/util/data/non-backported.txt` has no pattern matching them. `test/lint/lint-cppcheck-dash.py` (line 69) builds its file list exclusively from this allowlist, so the new harnesses are silently excluded from the Dash-specific cppcheck pass. Add an `src/test/fuzz/<file>.cpp` entry per new file (or a glob if appropriate) so the lint covers these new entry points.

In `.github/workflows/test-fuzz.yml`:
- [SUGGESTION] lines 129-132: detect_leaks=0 silently masks the BLS leak this PR explicitly acknowledges
  `ASAN_OPTIONS` sets `detect_leaks=0`, which disables LSan for every replayed corpus run. The PR description lists a known `libdashbls` leak surfaced by `bls_operations` campaigns, and this configuration ensures CI will never catch a regression of that leak — or any new leak — again. The line below already wires up `LSAN_OPTIONS=suppressions=...`, so the cleaner approach is to leave leak detection on and add a suppression for the known noisy frame in `test/sanitizer_suppressions/lsan`, rather than disabling the whole detector globally. As written, the suppressions file is dead code while `detect_leaks=0`.
- [SUGGESTION] lines 184-190: Use `-runs=1` to match the project's existing replay convention
  The project's existing `test/fuzz/test_runner.py:289` uses `-runs=1` for corpus replay; this workflow uses `-runs=0`. In current libFuzzer both effectively exit after seed corpus replay (the seed corpora are read unconditionally and the main loop's `TotalNumberOfRuns >= MaxNumberOfRuns` check then trips immediately), so this is not a correctness bug — Codex's blocking claim that `-runs=0` becomes unbounded fuzzing is overstated. However, `-runs=1` is the project convention and is safer against future libFuzzer changes. Worth aligning for consistency.

In `contrib/fuzz/seed_corpus_from_chain.py`:
- [SUGGESTION] lines 1140-1210: Several registered fuzz targets receive no synthetic seeds and fall back to the workflow's empty-corpus smoke run
  The workflow falls back to a 10-second empty-corpus run for any target without a corpus directory (test-fuzz.yml:159-181), so the seed generator needs to cover every target meant to participate in regression replay. The synthetic_seeds map omits `dash_dkg_justification_deserialize`, `dash_sig_shares_inv_deserialize`, `dash_batched_sig_shares_deserialize`, and `dash_mnhf_tx_deserialize` (only `dash_mnhf_tx_payload_deserialize` is generated indirectly via `extract_special_txs` type_map[7]). Unless these targets exist in the external dashpay/qa-assets corpus, they'll only ever take the smoke path instead of true replay, weakening the advertised regression gate.

In `src/test/fuzz/util.cpp`:
- [SUGGESTION] lines 332-341: Narrowing nVersion to int16_t drops upstream nVersion fuzz coverage
  `ConsumeTransaction` previously fed `nVersion` an arbitrary `int32_t`. After this change a non-CURRENT_VERSION transaction always has its top 16 bits zeroed, which means every shape where the high half of `nVersion` is non-zero (including all SPECIAL_VERSION + non-zero `nType` combinations) is now unreachable through this helper for every consumer (`block.cpp`, primitives_transaction targets, etc.). The comment correctly identifies the symptom (special-tx-type crashes swamping signal), but the right fix is to gate that branch — produce either a normal version with high bits zero, or a fuzzed full int32 with `tx_mut.nType` set explicitly — not to permanently narrow the range for all callers. As written, regressions in upstream `nVersion` handling are no longer reachable through this helper.

Comment thread .github/workflows/test-fuzz.yml Outdated
Comment thread .github/workflows/test-fuzz.yml
Comment thread contrib/fuzz/seed_corpus_from_chain.py
Comment thread src/test/fuzz/util.cpp Outdated
Comment thread src/test/fuzz/process_message.cpp Outdated
Comment thread .github/workflows/test-fuzz.yml
Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 441bbff3d3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread contrib/fuzz/continuous_fuzz_daemon.sh Outdated
Copy link
Copy Markdown
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Prior-review fixes have all landed: non-backported.txt now lists the new Dash fuzz cpps, the regression workflow uses -runs=1 with LSan suppressions, synthetic seeds were added for the previously-missing targets, and the daemon rejects zero target time. Remaining feedback is minor infrastructure-quality concerns: the long-running daemon still hard-disables LSan (the very thing the workflow stopped doing), the seed script aborts at import time outside a checkout, the corpus-replay step swallows exit codes so timeouts look like crashes, the MNAUTH skip TODO in process_message is already covered by process_message_dash, and util_dash.h is not pattern-matched by the cppcheck allowlist.

Reviewed commit: c613ff5

🟡 1 suggestion(s) | 💬 4 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `contrib/fuzz/continuous_fuzz_daemon.sh`:
- [SUGGESTION] lines 39-51: Continuous fuzz daemon hard-disables leak detection
  `DAEMON_ASAN_OPTIONS` sets `detect_leaks=0`, so the long-running campaigns added by this PR cannot catch leak regressions at all — including in the `libdashbls` area called out in the PR description. The CI workflow has already moved away from a global LSan disable in favour of `LSAN_OPTIONS=suppressions=test/sanitizer_suppressions/lsan`; the daemon should adopt the same approach to preserve leak signal. `count_crash_artifacts()` at line 51 also only matches `crash-*`/`timeout-*`/`oom-*`, so even if leak detection is re-enabled later, leak findings (`leak-*` artifacts) will not be counted as new crashes by the daemon's reporting path.

Comment thread contrib/fuzz/continuous_fuzz_daemon.sh Outdated
Comment thread .github/workflows/test-fuzz.yml
Comment thread contrib/fuzz/seed_corpus_from_chain.py Outdated
Comment thread src/test/fuzz/process_message.cpp Outdated
Comment thread test/util/data/non-backported.txt
Reject zero --time-per-target so the daemon never spins on no-op cycles, and
align ASAN/LSAN options with the fuzz regression workflow: keep leak detection
on, source suppressions from test/sanitizer_suppressions/lsan, and count/report
leak-* artifacts alongside crash/timeout/oom.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thepastaclaw thepastaclaw force-pushed the ci/fuzz-regression branch from c613ff5 to ecb73ac Compare May 4, 2026 12:48
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6e01dbca6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread contrib/fuzz/continuous_fuzz_daemon.sh Outdated
@thepastaclaw thepastaclaw force-pushed the ci/fuzz-regression branch from e6e01db to f68353c Compare May 4, 2026 14:11
Copy link
Copy Markdown
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

All five prior findings are resolved at f68353c (LSan re-enabled with suppressions, timeout/kill exits now classified separately from crashes, deferred PROTOCOL_VERSION lookup with --stream-version override, MNAUTH comment redirect, and util_dash.h listed in non-backported.txt). Codex flags two remaining gaps in the new fuzz infrastructure: synthetic-seed generation failures are downgraded to warnings (allowing Dash-only targets to silently fall back to 10s empty-input runs), and _run_helper_self_checks is dead code despite being the only deterministic guard for the new helpers. Both verified; neither is blocking.

Reviewed commit: f68353c

🟡 1 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `.github/workflows/test-fuzz.yml`:
- [SUGGESTION] lines 107-112: Synthetic seed generation failures silently demote targets to empty-input smoke tests
  `seed_corpus_from_chain.py --synthetic-only` is invoked with `|| echo "::warning::..."`, so any failure in the synthetic generator is non-fatal. Targets whose corpus directory ends up empty fall through to the empty-corpus branch at L171-195, which only runs the harness for 10s with no replay inputs. That means a regression in the seed generator can remove the only structured inputs for several Dash-only deserialize/roundtrip targets (governance vote files, BLS-IES blobs, LLMQ messages, etc.) without failing CI, and the job will still go green as long as those harnesses don't crash on empty input. For a workflow whose purpose is corpus-replay regression testing, this masks exactly the failure mode the synthetic generator was added to prevent. Either fail the step on non-zero exit, or assert post-generation that the expected per-target seed directories are non-empty.

Comment thread .github/workflows/test-fuzz.yml
Comment thread contrib/fuzz/seed_corpus_from_chain.py
@thepastaclaw thepastaclaw force-pushed the ci/fuzz-regression branch from f68353c to c11cf3c Compare May 4, 2026 21:48
Copy link
Copy Markdown
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Prior review's two carry-forward findings are partially addressed at c11cf3c: the synthetic-seed failure mask has been replaced with a hard-fail loaded_external gate, but two issues remain. The narrower coverage gap is that the new gate only checks that some external corpus loaded — Dash-only targets not in the synthetic generator can still silently fall through to the empty-input 10s smoke branch if the pinned dashpay/qa-assets snapshot omits them. The dead _run_helper_self_checks() carry-over is unchanged.

Reviewed commit: c11cf3c

🟡 1 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `.github/workflows/test-fuzz.yml`:
- [SUGGESTION] lines 170-193: Dash-only targets without synthetic seeds can silently fall through to empty-input smoke runs
  The new `loaded_external` gate (L62, L102-105) correctly prevents a global fall-back to synthetic-only/empty-corpus runs, but it only verifies that *some* external corpus loaded — it does not assert per-target presence. The synthetic generator at `contrib/fuzz/seed_corpus_from_chain.py:1169-1264` covers a fixed subset (CoinJoin, LLMQ messages, DKG, MNAuth, governance vote/instance/rec/file, BLS IES, MNHF). Other newly added Dash-only targets — `dash_governance_object_common_deserialize`, `dash_proreg_tx_deserialize`, `dash_quorum_data_request_deserialize`, `dash_quorum_snapshot_deserialize`, `dash_get_quorum_rotation_info_deserialize` — are populated only by the live-chain extract paths (`seed_corpus_from_chain.py:699, 851, 979, 1039, 1106`) or by the external `dashpay/qa-assets` snapshot. If the pinned snapshot ever omits or renames one of those directories, the per-target check at L170 sees an empty `corpus_dir`, runs the harness with no inputs for 10s, and the job goes green without ever replaying a structured input for that harness. For a regression workflow this is a coverage hole — the fix is to assert that targets known to require external/live-chain inputs have a non-empty corpus directory, and fail the job otherwise.

Comment thread .github/workflows/test-fuzz.yml
Comment thread contrib/fuzz/seed_corpus_from_chain.py
Copy link
Copy Markdown
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Re-review at c11cf3c confirms two non-blocking gaps in the fuzz regression infrastructure. _run_helper_self_checks() remains unreachable, and the per-target replay loop still allows empty-corpus 10s smoke runs for Dash-only targets that are absent from both the pinned external snapshot and the synthetic seed generator. No blocking issues observed in the workflow, daemon, or harness sources.

Reviewed commit: c11cf3c

🟡 1 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `.github/workflows/test-fuzz.yml`:
- [SUGGESTION] lines 170-194: Dash-only targets without seeds still fall back to empty-corpus smoke runs
  The new `loaded_external` gate (L102-L105) only proves that at least one external corpus source was fetched — it does not verify that each registered target actually has corpus inputs. The per-target replay loop at L170-L194 still treats a missing or empty `/tmp/fuzz_corpus/${target}` as a passing 10-second empty-input run, masking the absence of real regression coverage. I checked the pinned `dashpay/qa-assets` snapshot (`b4d1a7ce41a9a21d522381944ba84b60b1ad5b60`) together with the synthetic seed generator output: several Dash-only deserialize/roundtrip targets still have no inputs, including `dash_credit_pool_deserialize`, `dash_dmn_state_deserialize`, `dash_get_quorum_rotation_info_deserialize`, `dash_quorum_snapshot_deserialize`, and `dash_smn_list_entry_deserialize` plus related roundtrip variants. Those targets therefore get only smoke-only execution while CI still goes green. Consider failing (or maintaining an explicit allowlist for) targets whose corpus directory is empty after all three layers have been applied, so newly added Dash targets cannot silently degrade.

Comment thread contrib/fuzz/seed_corpus_from_chain.py
Comment thread .github/workflows/test-fuzz.yml
@thepastaclaw thepastaclaw force-pushed the ci/fuzz-regression branch from c11cf3c to 9df19ad Compare May 9, 2026 06:46
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9df19ad170

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .github/workflows/test-fuzz.yml
Comment thread src/test/fuzz/bls_operations.cpp
@thepastaclaw thepastaclaw force-pushed the ci/fuzz-regression branch from 9df19ad to e1d88f6 Compare May 9, 2026 07:48
@thepastaclaw
Copy link
Copy Markdown
Author

Addressed the latest fuzz regression review feedback in e1d88f6c3f:

  • Wired contrib/fuzz/seed_corpus_from_chain.py --self-check so helper/seed invariants are executable without --output-dir; --help still exits before any in-tree version lookup.
  • Added deterministic synthetic seeds for the remaining Dash evo deserialize/roundtrip targets (dash_credit_pool, dash_deterministic_mn, dash_dmn_state, dash_smn_list_entry) and added self-check presence/size guards.
  • Made the BLS IES roundtrip fuzz branch deterministic by deriving the encryption state from FuzzedDataProvider instead of calling InitEncrypt(1).

Validation run locally:

  • git diff --check
  • python3 -m py_compile contrib/fuzz/seed_corpus_from_chain.py
  • python3 contrib/fuzz/seed_corpus_from_chain.py --self-check
  • python3 contrib/fuzz/seed_corpus_from_chain.py --self-check --stream-version 70240
  • python3 contrib/fuzz/seed_corpus_from_chain.py --synthetic-only -o <tmpdir> and verified all new target directories are populated
  • python3 contrib/fuzz/seed_corpus_from_chain.py --help
  • python3 contrib/fuzz/seed_corpus_from_chain.py (verified it errors without --output-dir unless --self-check is used)
  • python3 -c "import yaml; yaml.safe_load(open('.github/workflows/test-fuzz.yml'))"

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1d88f6c3f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .github/workflows/test-fuzz.yml
- Workflow corpus replay now classifies timeout/kill exits (124/137/143)
  distinctly from generic crashes in both log and FAILED_TARGETS, and
  captures the exit code in the non-empty-corpus branch like the empty
  branch does.
- Defer src/version.h lookup in seed_corpus_from_chain.py until a stream
  version prefix is actually needed; --help no longer requires an in-tree
  checkout. Add --stream-version CLI flag and DASH_FUZZ_STREAM_VERSION
  env override.
- Correct synthetic LLMQ seeds to match C++ serialization:
  CRecoveredSig now includes msgHash; CSigSesAnn uses VARINT(sessionId)
  + llmqType + quorumHash + id + msgHash; CSigShare adds quorumMember
  and 96-byte sigShare; CDKGComplaint includes a full 96-byte BLS
  signature after both DYNBITSETs. Self-checks assert the corrected
  byte sizes.
- Replace stale TODO above the MNAUTH skip in process_message.cpp with
  a pointer to process_message_dash, which already exercises the
  Dash-aware MNAUTH setup.
- Add src/test/fuzz/util_dash.h to non-backported.txt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thepastaclaw thepastaclaw force-pushed the ci/fuzz-regression branch from e1d88f6 to fc87319 Compare May 9, 2026 08:57
Copy link
Copy Markdown
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Re-review at 9df19ad. The prior Dash-only smoke-fallback concern is resolved (the workflow now fails closed for dash_* targets lacking corpus). One carry-forward nit remains: _run_helper_self_checks() in contrib/fuzz/seed_corpus_from_chain.py is still unreachable, so its 'regression guard' assertions on governance helpers and LLMQ synthetic seed sizes never run in CI. No blocking issues.

Reviewed commit: 9df19ad

💬 1 nitpick(s)

Comment on lines +1281 to +1369
def _run_helper_self_checks():
"""Deterministic checks for new governance/final-commitment helpers."""
parsed_vote = parse_governance_vote_record(
"11" * 32 + "-2:1700000000:yes:funding:1",
"22" * 32,
)
assert parsed_vote["masternode_txid"] == "11" * 32
assert parsed_vote["masternode_n"] == 2
assert parsed_vote["signal"] == 1
assert parsed_vote["outcome"] == 1
vote_bytes = serialize_governance_vote(parsed_vote)
assert len(vote_bytes) == 32 + 4 + 32 + 4 + 4 + 8 + 1

vote_instance = serialize_vote_instance(1, 1700000000, 1690000000)
assert len(vote_instance) == 20
vote_rec = serialize_vote_rec({1: vote_instance, 2: serialize_vote_instance(2, 1700000100, 1690000000)})
assert vote_rec.startswith(b"\x02")
vote_file = serialize_governance_vote_file([vote_bytes])
assert vote_file[:4] == _serialize_int32(1)

payload = bytes.fromhex("0100") + _serialize_uint32(42) + b"\xaa\xbb\xcc"
assert _final_commitment_from_tx_payload(payload.hex()) == b"\xaa\xbb\xcc"

with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
create_synthetic_seeds(tmp_path)
required_targets = [
"dash_final_commitment_deserialize",
"dash_final_commitment_roundtrip",
"dash_governance_vote_deserialize",
"dash_vote_instance_deserialize",
"dash_vote_rec_deserialize",
"dash_governance_vote_file_deserialize",
"dash_bls_ies_encrypted_blob_deserialize",
"dash_bls_ies_encrypted_blob_roundtrip",
"dash_bls_ies_multi_recipient_blobs_deserialize",
"dash_bls_ies_multi_recipient_blobs_roundtrip",
"dash_coinjoin_entry_deserialize",
"dash_coinjoin_entry_roundtrip",
"dash_dkg_complaint_deserialize",
"dash_dkg_complaint_roundtrip",
"dash_dkg_justification_deserialize",
"dash_dkg_justification_roundtrip",
"dash_sig_shares_inv_deserialize",
"dash_sig_shares_inv_roundtrip",
"dash_batched_sig_shares_deserialize",
"dash_batched_sig_shares_roundtrip",
"dash_recovered_sig_deserialize",
"dash_recovered_sig_roundtrip",
"dash_sig_ses_ann_deserialize",
"dash_sig_ses_ann_roundtrip",
"dash_sig_share_deserialize",
"dash_sig_share_roundtrip",
"dash_mnauth_deserialize",
"dash_mnauth_roundtrip",
"dash_mnhf_tx_deserialize",
]
missing = [target for target in required_targets if not any((tmp_path / target).iterdir())]
assert not missing, f"missing synthetic seeds for: {', '.join(missing)}"

# Assert the LLMQ seed sizes match the C++ serialization layouts so the
# synthetic seeds aren't silently truncated again (regression guard).
def _seed_bytes(target):
files = list((tmp_path / target).iterdir())
assert len(files) == 1, f"{target}: expected one synthetic seed, got {len(files)}"
return files[0].read_bytes()

# The Dash deserialize/roundtrip wrappers prepend a 4-byte stream version.
prefix = len(_stream_version_prefix())
# CRecoveredSig: 1 + 32 + 32 + 32 + 96 = 193
assert len(_seed_bytes("dash_recovered_sig_deserialize")) == prefix + 193
# CSigSesAnn (VARINT(0)=1 byte): 1 + 1 + 32 + 32 + 32 = 98
assert len(_seed_bytes("dash_sig_ses_ann_deserialize")) == prefix + 98
# CSigShare: 1 + 32 + 2 + 32 + 32 + 96 = 195
assert len(_seed_bytes("dash_sig_share_deserialize")) == prefix + 195
# CDKGComplaint with empty bitsets: 1 + 32 + 32 + 1 + 1 + 96 = 163
assert len(_seed_bytes("dash_dkg_complaint_deserialize")) == prefix + 163

# --stream-version override path: setting the override must not require
# src/version.h, and _stream_version_prefix must round-trip the value.
global _STREAM_VERSION_OVERRIDE, _STREAM_VERSION_CACHE
saved_override, saved_cache = _STREAM_VERSION_OVERRIDE, _STREAM_VERSION_CACHE
try:
_STREAM_VERSION_OVERRIDE = 0x12345678
_STREAM_VERSION_CACHE = None
assert _stream_version_prefix() == b"\x78\x56\x34\x12"
finally:
_STREAM_VERSION_OVERRIDE = saved_override
_STREAM_VERSION_CACHE = saved_cache
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: _run_helper_self_checks() is dead code — its 'regression guard' assertions never execute

_run_helper_self_checks() (L1281) is the only place that asserts the new governance vote serializers (parse_governance_vote_record, serialize_governance_vote, serialize_vote_instance, serialize_vote_rec, serialize_governance_vote_file), the _final_commitment_from_tx_payload() extractor, the --stream-version override round-trip, and — explicitly labelled as a regression guard at L1341–L1357 — the LLMQ synthetic-seed byte-size invariants (CRecoveredSig=193, CSigSesAnn=98, CSigShare=195, CDKGComplaint=163). However, main() (L1372) never calls it, the argparse block (L1394–L1403) exposes no --self-check flag, and a repo-wide grep shows no other call site. As-written, the regression-guard comment is misleading: a future change that breaks any of those layouts will not be caught by CI or by --synthetic-only runs. Wire it into main() (e.g., behind a --self-check flag, or unconditionally as part of --synthetic-only) or migrate the assertions into a unit test that actually executes in CI.

source: ['claude', 'codex']

Copy link
Copy Markdown
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Verified all three Claude findings against the worktree at fc87319. All are valid non-blocking observations about fuzz infrastructure: the new --self-check entrypoint exists but no CI step invokes it; six dash_* deserialize/roundtrip targets have no synthetic seeds and depend entirely on pinned external corpus; and the cumulative MoneyRange assert in asset_lock_unlock cannot fire under the harness's own bounds. Codex reported no findings on this SHA. No blockers.

Reviewed commit: fc87319

Fresh dispatcher run for this queue item. A same-SHA review already existed, so this records the fresh verification without duplicating inline threads.

Code Review

Reviewed commit: fc87319

🟡 1 suggestion(s) | 💬 2 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `contrib/fuzz/seed_corpus_from_chain.py`:
- [SUGGESTION] lines 1297-1427: Several dash_* deserialize/roundtrip targets have no synthetic seeds
  The CI workflow runs `seed_corpus_from_chain.py --synthetic-only` at .github/workflows/test-fuzz.yml:110, which skips the live `extract_special_txs()` / `extract_masternode_list()` extractors. Since the workflow now hard-fails for any `dash_*` target without corpus inputs (test-fuzz.yml:194-201, with `DASH_SMOKE_ONLY_ALLOWLIST` empty), these registered Dash-only targets must come from the pinned `dashpay/qa-assets` snapshot — they are absent from the `synthetic_seeds` dict at lines 1297-1427:

- `dash_proreg_tx_deserialize` / `_roundtrip` (only seeded live by extract_masternode_list at line 851)
- `dash_proupserv_tx_*`, `dash_cbtx_*`, `dash_asset_lock_payload_*`, `dash_asset_unlock_payload_*` (only seeded live by extract_special_txs type_map at lines 594-604)
- `dash_dmn_state_diff_deserialize` / `_roundtrip` (no extractor, no synthetic seed — `dmn_state_diff` does not appear anywhere in the script)

If a future bump of `DASH_QA_ASSETS_SHA` (test-fuzz.yml:56) drops or renames these directories, the regression job breaks with a missing-corpus failure even though no Dash code changed. Adding minimal synthetic entries — the wire layouts for ProRegTx/ProUpServTx/CbTx/AssetLockPayload/AssetUnlockPayload/CDeterministicMNStateDiff are already used by adjacent synthetic builders — would make the workflow self-contained and match how the rest of the dash_* targets are now seeded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants