Skip to content

Comments

test: add roundtrip serialize/deserialize fuzz targets for Dash types#7162

Open
thepastaclaw wants to merge 4 commits intodashpay:developfrom
thepastaclaw:fuzz/roundtrip-dash
Open

test: add roundtrip serialize/deserialize fuzz targets for Dash types#7162
thepastaclaw wants to merge 4 commits intodashpay:developfrom
thepastaclaw:fuzz/roundtrip-dash

Conversation

@thepastaclaw
Copy link

@thepastaclaw thepastaclaw commented Feb 22, 2026

Summary

Add 37 roundtrip serialize/deserialize fuzz targets for Dash-specific types. This is Phase 2 of the Dash Core Fuzzing Initiative.

Motivation

All 106 existing fuzz targets in Dash Core are inherited from Bitcoin Core. None cover Dash-specific code paths (~24,000 lines in evo/, llmq/, governance/, coinjoin/, bls/). Phase 1 (PR #7161) added deserialization-only targets. This PR adds roundtrip consistency targets that verify:

deserialize(bytes) → serialize() → deserialize() → serialize() → assert bytes match

This catches serialization asymmetries, field truncation, and version-dependent encoding bugs.

What's included

37 new fuzz targets covering:

  • evo/ — CDeterministicMN, CSimplifiedMNListEntry, CBLSPublicKey, ProTx types (Register, Update Service/Registrar/Revoke), CreditFunding, AssetLock/Unlock, MNHFSignal
  • llmq/ — CFinalCommitment, CDKGPrematureCommitment, CDKGComplaint, CDKGContribution, CDKGJustification, CInstantSendLock, CChainLockSig, CRecoveredSig, CQuorumRotationInfo
  • governance/ — CGovernanceObject, CGovernanceVote
  • coinjoin/ — CCoinJoinEntry, CCoinJoinQueue, CCoinJoinBroadcastTx, CCoinJoinStatusUpdate, CCoinJoinAccept, CCoinJoinComplete
  • bls/ — CBLSPublicKey, CBLSSecretKey, CBLSSignature (with special-case generation from random bytes)

Special handling

  • CDeterministicMN: Uses CDeterministicMN(0) (non-default-constructible)
  • Version extraction: First 4 fuzz bytes used as protocol version — allows the fuzzer to explore version-dependent serialization paths
  • BLS types: Separate targets for key/sig validity checks with proper random generation

Testing

  • make -C src/test/fuzz -j$(nproc) — all targets compile
  • Verified each target links and runs with empty corpus

Part of

Validation

What was tested:

  • make -C src/test/fuzz -j$(nproc) — compiled all 37 new roundtrip fuzz targets
  • Verified each target links and runs with an empty corpus

Results:

  • All 37 fuzz targets compiled and ran successfully
  • CI full suite: all checks passed
    • linux64-build / Build source — pass
    • linux64-test / Test source — pass
    • linux64_nowallet-build — pass; linux64_nowallet-test — pass
    • linux64_sqlite-build — pass; linux64_sqlite-test — pass
    • linux64_tsan-build — pass; linux64_tsan-test — pass
    • linux64_ubsan-build — pass; linux64_ubsan-test — pass
    • mac-build / Build source — pass
    • win64-build / Build source — pass

Environment: Local macOS arm64 (fuzz build); GitHub Actions CI (linux64_fuzz + full matrix)

…or Dash types

Phase 2 of the Dash fuzzing initiative. Tests that for each
Dash-specific type, deserialize(serialize(deserialize(input)))
produces byte-identical output, catching asymmetric
serialization bugs.
@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Walkthrough

Adds a new fuzz test source and a fuzzing implementation that performs roundtrip serialization/deserialization for many Dash-specific types. Introduces a generic DashRoundtripFromFuzzingInput routine, a testing setup initialization, a custom dash_invalid_fuzzing_input_exception, and multiple FUZZ_TARGET_DASH_ROUNDTRIP registrations. Clears mnSkipList before unserializing in CQuorumSnapshot::Unserialize. Updates Makefile.test.include and test/util/data/non-backported.txt to include the new fuzz source.

Sequence Diagram(s)

sequenceDiagram
    participant Fuzzer
    participant Setup as BasicTestingSetup
    participant Stream as FuzzingStream
    participant Deser as Deserializer
    participant Obj as Object(T)
    participant Ser as Serializer
    participant Comparator as Comparator

    Fuzzer->>Setup: initialize_roundtrip_dash()
    Fuzzer->>Stream: provide fuzz buffer
    Stream->>Deser: read version
    Deser->>Obj: unserialize from Stream
    Deser-->>Fuzzer: throw dash_invalid_fuzzing_input_exception on failure
    Obj->>Ser: serialize to bytes
    Ser->>Deser: deserialize bytes into Obj'
    Deser->>Comparator: compare original bytes vs reserialized bytes
    Comparator-->>Fuzzer: assert equal / report mismatch
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding 37 roundtrip serialize/deserialize fuzz targets for Dash-specific types, which aligns perfectly with the changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the motivation, specific types covered, special handling requirements, testing results, and context within the larger Fuzzing Initiative.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@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

🧹 Nitpick comments (2)
src/test/fuzz/roundtrip_dash.cpp (2)

76-76: Inconsistent span construction — prefer MakeByteSpan uniformly

Line 76 uses Span<const std::byte>{ds2} while line 86 uses MakeByteSpan(ds2). Both are equivalent here (since nReadPos == 0 on a freshly-written stream), but mixing idioms is inconsistent.

🔄 Use `MakeByteSpan` consistently
-    CDataStream ds3(Span<const std::byte>{ds2}, SER_NETWORK, ds.GetVersion());
+    CDataStream ds3(MakeByteSpan(ds2), SER_NETWORK, ds.GetVersion());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/fuzz/roundtrip_dash.cpp` at line 76, Replace the direct Span
construction with the helper to keep idioms consistent: change the CDataStream
construction that currently uses Span<const std::byte>{ds2} to use
MakeByteSpan(ds2) instead so both CDataStream creations use MakeByteSpan and the
code uniformly handles byte spans (refer to the CDataStream constructor call and
the ds2 variable).

49-49: g_setup is set but never used — dead code

g_setup is assigned in initialize_roundtrip_dash() but no fuzz target reads it. The static local testing_setup inside initialize_roundtrip_dash still performs the necessary global-context initialization, so g_setup itself is redundant.

🧹 Remove the unused pointer
-const BasicTestingSetup* g_setup;
-
 struct dash_invalid_fuzzing_input_exception : public std::exception {
 void initialize_roundtrip_dash()
 {
     static const auto testing_setup = MakeNoLogFileContext<>();
-    g_setup = testing_setup.get();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/fuzz/roundtrip_dash.cpp` at line 49, g_setup is an unused global
pointer left over from initialization; remove the dead variable and its
assignment to avoid unused-state confusion. Delete the declaration "const
BasicTestingSetup* g_setup;" and remove any assignments to g_setup in
initialize_roundtrip_dash(), leaving the static local testing_setup to perform
the global initialization; ensure no other code references g_setup after removal
and run a build to confirm no remaining uses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/test/fuzz/roundtrip_dash.cpp`:
- Around line 1-3: Add the missing entry for roundtrip_dash.cpp to the
non-backported list: open test/util/data/non-backported.txt and insert the line
"src/test/fuzz/roundtrip_dash.cpp" into the alphabetical ordering, placing it
after the existing src/test/evo*.cpp entries so the file is included for
clang-format enforcement.

---

Nitpick comments:
In `@src/test/fuzz/roundtrip_dash.cpp`:
- Line 76: Replace the direct Span construction with the helper to keep idioms
consistent: change the CDataStream construction that currently uses Span<const
std::byte>{ds2} to use MakeByteSpan(ds2) instead so both CDataStream creations
use MakeByteSpan and the code uniformly handles byte spans (refer to the
CDataStream constructor call and the ds2 variable).
- Line 49: g_setup is an unused global pointer left over from initialization;
remove the dead variable and its assignment to avoid unused-state confusion.
Delete the declaration "const BasicTestingSetup* g_setup;" and remove any
assignments to g_setup in initialize_roundtrip_dash(), leaving the static local
testing_setup to perform the global initialization; ensure no other code
references g_setup after removal and run a build to confirm no remaining uses.

Comment on lines +1 to +3
// Copyright (c) 2026 The Dash Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check if roundtrip_dash.cpp exists
echo "=== Checking if roundtrip_dash.cpp exists ==="
find . -name "roundtrip_dash.cpp" -type f 2>/dev/null

# Find and check the non-backported.txt file
echo -e "\n=== Locating non-backported.txt ==="
find . -path "*/test/util/data/non-backported.txt" -type f 2>/dev/null

# Check if roundtrip_dash is in non-backported.txt
echo -e "\n=== Checking if roundtrip_dash is in non-backported.txt ==="
grep -n "roundtrip_dash" test/util/data/non-backported.txt 2>/dev/null || echo "Not found"

# Show entries around 'roundtrip' and 'fuzz' for context
echo -e "\n=== Entries containing 'roundtrip' or 'fuzz' in non-backported.txt ==="
grep -n -E "roundtrip|fuzz" test/util/data/non-backported.txt 2>/dev/null || echo "None found"

# Show a sample of the file to understand format
echo -e "\n=== Sample entries from non-backported.txt (first 20 lines) ==="
head -20 test/util/data/non-backported.txt 2>/dev/null || echo "File not accessible"

Repository: dashpay/dash

Length of output: 782


🏁 Script executed:

#!/bin/bash

# Show entries related to src/test
echo "=== Entries starting with 'src/test' ==="
grep -n "^src/test" test/util/data/non-backported.txt | head -20

# Count total lines
echo -e "\n=== Total lines in non-backported.txt ==="
wc -l test/util/data/non-backported.txt

# Show the entire file to understand complete context
echo -e "\n=== Full content of non-backported.txt ==="
cat test/util/data/non-backported.txt

Repository: dashpay/dash

Length of output: 1977


Add src/test/fuzz/roundtrip_dash.cpp to test/util/data/non-backported.txt

This Dash-specific .cpp file is not currently listed in test/util/data/non-backported.txt, which is required for clang-format enforcement. Insert src/test/fuzz/roundtrip_dash.cpp alphabetically after src/test/evo*.cpp (line 57).

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

In `@src/test/fuzz/roundtrip_dash.cpp` around lines 1 - 3, Add the missing entry
for roundtrip_dash.cpp to the non-backported list: open
test/util/data/non-backported.txt and insert the line
"src/test/fuzz/roundtrip_dash.cpp" into the alphabetical ordering, placing it
after the existing src/test/evo*.cpp entries so the file is included for
clang-format enforcement.

@thepastaclaw
Copy link
Author

Note: This PR is a companion to #7161 (deserialization fuzz targets) but not dependent on it. Both add new fuzz target files independently. If #7161 merges first, this will need a trivial rebase of src/Makefile.test.include (adjacent line in the fuzz source list).

This is Phase 2 of the Dash fuzzing initiative (tracked in thepastaclaw/tracker#108).

@thepastaclaw thepastaclaw changed the title test(fuzz): add roundtrip serialize/deserialize targets for Dash types test: add roundtrip serialize/deserialize fuzz targets for Dash types Feb 22, 2026
@thepastaclaw thepastaclaw marked this pull request as draft February 22, 2026 19:00
@thepastaclaw
Copy link
Author

Phase 2 validation update: 35/36 roundtrip targets pass, 1 crashes ().

I marked this PR draft while I triage/fix the failure. Tracking fix in thepastaclaw/tracker#128.

@thepastaclaw
Copy link
Author

Correction: failed target is dash_quorum_snapshot_roundtrip.

…ading

CQuorumSnapshot::Unserialize used push_back to append entries to
mnSkipList without clearing it first. This means deserializing into
a non-empty CQuorumSnapshot object would corrupt mnSkipList by
doubling its contents.

While ReadFixedBitSet properly replaces activeQuorumMembers via
resize(), mnSkipList had no such clearing, creating a serialization
asymmetry: serialize(deserialize(data)) != data when the target
object is pre-populated.

Found by the dash_quorum_snapshot_roundtrip fuzz target.
@thepastaclaw
Copy link
Author

Fuzz validation complete after CQuorumSnapshot fix: 36/36 roundtrip targets pass clean (60s each, ASan+UBSan+libFuzzer on aarch64 Linux).

@thepastaclaw thepastaclaw marked this pull request as ready for review February 22, 2026 21:15
Copy link

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/llmq/snapshot.h (1)

187-206: ⚠️ Potential issue | 🟠 Major

CQuorumRotationInfo::Unserialize has the same accumulation bug just fixed in CQuorumSnapshot.

All three vectors are appended via push_back without a preceding clear():

  • lastCommitmentPerIndex (line 191)
  • quorumSnapshotList (line 198)
  • mnListDiffList (line 205)

Re-deserializing into a non-empty CQuorumRotationInfo (exactly what the roundtrip fuzz harness does) will cause each vector to double, producing the same serialize(deserialize(data)) != data asymmetry that was just fixed in CQuorumSnapshot. The fuzz target for CQuorumRotationInfo roundtrip will likely reproduce this.

🐛 Proposed fix — clear all three vectors before reading
     size_t cnt = ReadCompactSize(s);
+    lastCommitmentPerIndex.clear();
     for ([[maybe_unused]] const auto _ : irange::range(cnt)) {
         CFinalCommitment qc;
         ::Unserialize(s, qc);
         lastCommitmentPerIndex.push_back(std::move(qc));
     }

     cnt = ReadCompactSize(s);
+    quorumSnapshotList.clear();
     for ([[maybe_unused]] const auto _ : irange::range(cnt)) {
         CQuorumSnapshot snap;
         ::Unserialize(s, snap);
         quorumSnapshotList.push_back(std::move(snap));
     }

     cnt = ReadCompactSize(s);
+    mnListDiffList.clear();
     for ([[maybe_unused]] const auto _ : irange::range(cnt)) {
         CSimplifiedMNListDiff mnlist;
         ::Unserialize(s, mnlist);
         mnListDiffList.push_back(std::move(mnlist));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llmq/snapshot.h` around lines 187 - 206, In
CQuorumRotationInfo::Unserialize the vectors lastCommitmentPerIndex,
quorumSnapshotList, and mnListDiffList are appended to during deserialization;
before reading each compact-size loop in CQuorumRotationInfo::Unserialize call
lastCommitmentPerIndex.clear(), quorumSnapshotList.clear(), and
mnListDiffList.clear() respectively so each push_back populates fresh vectors
and prevents doubling when deserializing into an existing object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/llmq/snapshot.h`:
- Around line 187-206: In CQuorumRotationInfo::Unserialize the vectors
lastCommitmentPerIndex, quorumSnapshotList, and mnListDiffList are appended to
during deserialization; before reading each compact-size loop in
CQuorumRotationInfo::Unserialize call lastCommitmentPerIndex.clear(),
quorumSnapshotList.clear(), and mnListDiffList.clear() respectively so each
push_back populates fresh vectors and prevents doubling when deserializing into
an existing object.

Required for clang-format CI enforcement on Dash-specific files.
Copy link

@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.

🧹 Nitpick comments (1)
test/util/data/non-backported.txt (1)

53-53: LGTM — entry is correctly a .cpp file for a new non-backported Dash target.

Minor nit: the src/test/ block in this file is roughly alphabetical, and fuzz/roundtrip_dash.cpp (f) lands between two b-prefixed entries. It would read more consistently if moved after src/test/evo*.cpp (line 58), i.e. between the evo and governance groups.

🔧 Suggested re-ordering
-src/test/block_reward_reallocation_tests.cpp
-src/test/fuzz/roundtrip_dash.cpp
-src/test/bls_tests.cpp
+src/test/block_reward_reallocation_tests.cpp
+src/test/bls_tests.cpp

…and insert later:

 src/test/evo*.cpp
+src/test/fuzz/roundtrip_dash.cpp
 src/test/llmq*.cpp

Based on learnings: test/util/data/non-backported.txt must include only .cpp/.h files (satisfied), and new non-backported files must be added to this list (satisfied).

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

In `@test/util/data/non-backported.txt` at line 53, The list entry for the new
non-backported test file is out of alphabetical order: move the entry
"roundtrip_dash.cpp" so it appears after the existing evo*.cpp entries and
before the governance group in the src/test block of non-backported.txt to keep
the section alphabetically consistent; ensure you only relocate the single line
(don’t change file extension or other entries) so the file remains listed and
still under the .cpp/.h-only rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/util/data/non-backported.txt`:
- Line 53: The list entry for the new non-backported test file is out of
alphabetical order: move the entry "roundtrip_dash.cpp" so it appears after the
existing evo*.cpp entries and before the governance group in the src/test block
of non-backported.txt to keep the section alphabetically consistent; ensure you
only relocate the single line (don’t change file extension or other entries) so
the file remains listed and still under the .cpp/.h-only rule.

Copy link

@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.

🧹 Nitpick comments (2)
src/test/fuzz/roundtrip_dash.cpp (2)

75-75: Copy-constructing obj2 from obj weakens the roundtrip invariant.

T obj2 = obj seeds obj2 with the already-deserialized state before ds3 >> obj2 overwrites it. If any Unserialize implementation appends to (rather than resets) a container field, both serializations would still match because obj2 started as a valid copy. A freshly default-constructed obj2 would be a stricter probe of that invariant.

This matters only for the explicit-T& overload; the zero-argument overload (line 92) already starts from a default-constructed object.

♻️ Proposed fix (default-constructible types only)
-    T obj2 = obj;
-    CDataStream ds3(Span<const std::byte>{ds2}, SER_NETWORK, ds.GetVersion());
+    T obj2{};
+    CDataStream ds3(Span<const std::byte>{ds2}, SER_NETWORK, ds.GetVersion());

For non-default-constructible types (e.g. CDeterministicMN) the copy-construct path must be retained, so a if constexpr (std::is_default_constructible_v<T>) guard or a dedicated overload can be used.

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

In `@src/test/fuzz/roundtrip_dash.cpp` at line 75, The copy-construction T obj2 =
obj weakens the roundtrip test because obj2 inherits deserialized state before
ds3 >> obj2; change to start obj2 default-constructed when possible: in the
function in roundtrip_dash.cpp, replace the copy-path with a conditional that
does if constexpr (std::is_default_constructible_v<T>) { T obj2{}; ds3 >> obj2;
} else { T obj2 = obj; ds3 >> obj2; } (or add a dedicated overload) so that
default-constructible types get a fresh object and non-default-constructible
types (e.g. CDeterministicMN) keep the copy-construct fallback; ensure
references to ds3 >> obj2 and Unserialize remain unchanged.

49-50: g_setup is written but never read.

The raw pointer set via g_setup = testing_setup.get() (line 101) is never dereferenced anywhere in this file. The static local in initialize_roundtrip_dash is what drives initialization; storing the pointer in g_setup is redundant. Consider removing the global or, if kept for future use, annotating with a [[maybe_unused]] attribute to suppress potential warnings.

♻️ Proposed cleanup
-const BasicTestingSetup* g_setup;
 void initialize_roundtrip_dash()
 {
     static const auto testing_setup = MakeNoLogFileContext<>();
-    g_setup = testing_setup.get();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/fuzz/roundtrip_dash.cpp` around lines 49 - 50, The global pointer
g_setup is assigned from testing_setup.get() but never read; remove the
redundant global declaration and the assignment in initialize_roundtrip_dash
(references: g_setup and testing_setup.get()), or if you prefer to keep it for
future use annotate the declaration with [[maybe_unused]] to suppress warnings;
ensure any removed assignment/variable has no other uses and leave the static
local in initialize_roundtrip_dash as the sole initializer driving setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/test/fuzz/roundtrip_dash.cpp`:
- Around line 1-3: The PR added src/test/fuzz/roundtrip_dash.cpp to
test/util/data/non-backported.txt but that entry is already present; locate the
non-backported.txt list (the entry referencing roundtrip_dash.cpp) and remove
the duplicate line so each file appears only once, ensuring the unique
identifier "src/test/fuzz/roundtrip_dash.cpp" is not listed twice.

---

Nitpick comments:
In `@src/test/fuzz/roundtrip_dash.cpp`:
- Line 75: The copy-construction T obj2 = obj weakens the roundtrip test because
obj2 inherits deserialized state before ds3 >> obj2; change to start obj2
default-constructed when possible: in the function in roundtrip_dash.cpp,
replace the copy-path with a conditional that does if constexpr
(std::is_default_constructible_v<T>) { T obj2{}; ds3 >> obj2; } else { T obj2 =
obj; ds3 >> obj2; } (or add a dedicated overload) so that default-constructible
types get a fresh object and non-default-constructible types (e.g.
CDeterministicMN) keep the copy-construct fallback; ensure references to ds3 >>
obj2 and Unserialize remain unchanged.
- Around line 49-50: The global pointer g_setup is assigned from
testing_setup.get() but never read; remove the redundant global declaration and
the assignment in initialize_roundtrip_dash (references: g_setup and
testing_setup.get()), or if you prefer to keep it for future use annotate the
declaration with [[maybe_unused]] to suppress warnings; ensure any removed
assignment/variable has no other uses and leave the static local in
initialize_roundtrip_dash as the sole initializer driving setup.

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.

1 participant