1919import argparse
2020import hashlib
2121import json
22+ import os
2223import re
2324import subprocess
2425import sys
@@ -52,9 +53,9 @@ def _read_protocol_version():
5253 return int (match .group (1 ))
5354
5455
55- # Must match src/ version.h PROTOCOL_VERSION. Several fuzz harnesses read a
56- # 4-byte little-endian int from the start of the buffer and use it as the
57- # stream version before deserializing the object:
56+ # The stream version is needed by several fuzz harnesses that read a 4-byte
57+ # little-endian int from the start of the buffer and use it as the stream
58+ # version before deserializing the object:
5859# * The Dash-specific helpers DashDeserializeFromFuzzingInput /
5960# DashRoundtripFromFuzzingInput (src/test/fuzz/deserialize_dash.cpp,
6061# src/test/fuzz/roundtrip_dash.cpp), used by dash_*_deserialize and
@@ -65,10 +66,37 @@ def _read_protocol_version():
6566# via DeserializeFromFuzzingInput when no explicit protocol_version is
6667# passed.
6768# Chain data we extract is serialized at PROTOCOL_VERSION, so we prepend
68- # that value to seeds for those targets.
69- PROTOCOL_VERSION = _read_protocol_version ()
70- STREAM_VERSION = PROTOCOL_VERSION
71- STREAM_VERSION_PREFIX = STREAM_VERSION .to_bytes (4 , byteorder = "little" , signed = False )
69+ # that value to seeds for those targets. The lookup is deferred to first use
70+ # so `--help` and callers that supply --stream-version / DASH_FUZZ_STREAM_VERSION
71+ # don't require an in-tree src/version.h.
72+ _STREAM_VERSION_OVERRIDE = None
73+ _STREAM_VERSION_CACHE = None
74+
75+
76+ def _resolve_stream_version ():
77+ """Return the stream version, preferring an explicit override and falling back
78+ to parsing src/version.h. Cached after first successful resolution."""
79+ global _STREAM_VERSION_CACHE
80+ if _STREAM_VERSION_CACHE is not None :
81+ return _STREAM_VERSION_CACHE
82+ if _STREAM_VERSION_OVERRIDE is not None :
83+ _STREAM_VERSION_CACHE = _STREAM_VERSION_OVERRIDE
84+ return _STREAM_VERSION_CACHE
85+ env_override = os .environ .get ("DASH_FUZZ_STREAM_VERSION" )
86+ if env_override :
87+ try :
88+ _STREAM_VERSION_CACHE = int (env_override )
89+ except ValueError as e :
90+ raise RuntimeError (
91+ f"DASH_FUZZ_STREAM_VERSION must be an integer, got { env_override !r} "
92+ ) from e
93+ return _STREAM_VERSION_CACHE
94+ _STREAM_VERSION_CACHE = _read_protocol_version ()
95+ return _STREAM_VERSION_CACHE
96+
97+
98+ def _stream_version_prefix ():
99+ return _resolve_stream_version ().to_bytes (4 , byteorder = "little" , signed = False )
72100
73101# Non-Dash targets (outside the dash_* naming convention) whose harnesses
74102# also consume the 4-byte stream version prefix described above.
@@ -113,7 +141,7 @@ def save_corpus_input(output_dir, target_name, data_hex):
113141 return False
114142
115143 if _needs_stream_version_prefix (target_name ):
116- raw_bytes = STREAM_VERSION_PREFIX + raw_bytes
144+ raw_bytes = _stream_version_prefix () + raw_bytes
117145
118146 filename = hashlib .sha256 (raw_bytes ).hexdigest ()[:16 ]
119147 filepath = target_dir / filename
@@ -1169,21 +1197,27 @@ def create_synthetic_seeds(output_dir):
11691197 ((1 ).to_bytes (2 , "little" ) + _serialize_uint32 (0 ) + minimal_final_commitment ).hex (),
11701198 ],
11711199 "dash_recovered_sig_deserialize" : [
1172- "64" + "00" * 32 + "00" * 32 + "00" * 96 , # llmqType + quorumHash + id + sig
1200+ # CRecoveredSig: llmqType (uint8) + quorumHash (32) + id (32) + msgHash (32) + sig (96)
1201+ "64" + "00" * 32 + "00" * 32 + "00" * 32 + "00" * 96 ,
11731202 ],
11741203 "dash_sig_ses_ann_deserialize" : [
1175- "64" + "00" * 32 + "00000000" + "00" * 32 , # llmqType + quorumHash + nSessionId + id
1204+ # CSigSesAnn: VARINT(sessionId=0) + llmqType (uint8) + quorumHash (32) + id (32) + msgHash (32)
1205+ "00" + "64" + "00" * 32 + "00" * 32 + "00" * 32 ,
11761206 ],
11771207 "dash_sig_share_deserialize" : [
1178- "64" + "00" * 32 + "00000000" + "00" * 32 + "0000" + "00" * 96 ,
1208+ # CSigShare: llmqType (uint8) + quorumHash (32) + quorumMember (uint16 LE)
1209+ # + id (32) + msgHash (32) + sigShare (96, BLS lazy)
1210+ "64" + "00" * 32 + "0000" + "00" * 32 + "00" * 32 + "00" * 96 ,
11791211 ],
11801212 # MNAuth
11811213 "dash_mnauth_deserialize" : [
11821214 "00" * 32 + "00" * 32 + "00" * 96 , # proRegTxHash + signChallenge + sig
11831215 ],
11841216 # DKG messages
11851217 "dash_dkg_complaint_deserialize" : [
1186- "64" + "00" * 32 + "00" * 32 + "0000" + "00" , # minimal
1218+ # CDKGComplaint: llmqType + quorumHash + proTxHash + DYNBITSET(badMembers)
1219+ # + DYNBITSET(complainForMembers) + sig (96 bytes)
1220+ "64" + "00" * 32 + "00" * 32 + "00" + "00" + "00" * 96 ,
11871221 ],
11881222 "dash_dkg_justification_deserialize" : [
11891223 # llmqType (uint8) + quorumHash (32) + proTxHash (32) +
@@ -1283,17 +1317,57 @@ def _run_helper_self_checks():
12831317 "dash_bls_ies_multi_recipient_blobs_roundtrip" ,
12841318 "dash_coinjoin_entry_deserialize" ,
12851319 "dash_coinjoin_entry_roundtrip" ,
1320+ "dash_dkg_complaint_deserialize" ,
1321+ "dash_dkg_complaint_roundtrip" ,
12861322 "dash_dkg_justification_deserialize" ,
12871323 "dash_dkg_justification_roundtrip" ,
12881324 "dash_sig_shares_inv_deserialize" ,
12891325 "dash_sig_shares_inv_roundtrip" ,
12901326 "dash_batched_sig_shares_deserialize" ,
12911327 "dash_batched_sig_shares_roundtrip" ,
1328+ "dash_recovered_sig_deserialize" ,
1329+ "dash_recovered_sig_roundtrip" ,
1330+ "dash_sig_ses_ann_deserialize" ,
1331+ "dash_sig_ses_ann_roundtrip" ,
1332+ "dash_sig_share_deserialize" ,
1333+ "dash_sig_share_roundtrip" ,
1334+ "dash_mnauth_deserialize" ,
1335+ "dash_mnauth_roundtrip" ,
12921336 "dash_mnhf_tx_deserialize" ,
12931337 ]
12941338 missing = [target for target in required_targets if not any ((tmp_path / target ).iterdir ())]
12951339 assert not missing , f"missing synthetic seeds for: { ', ' .join (missing )} "
12961340
1341+ # Assert the LLMQ seed sizes match the C++ serialization layouts so the
1342+ # synthetic seeds aren't silently truncated again (regression guard).
1343+ def _seed_bytes (target ):
1344+ files = list ((tmp_path / target ).iterdir ())
1345+ assert len (files ) == 1 , f"{ target } : expected one synthetic seed, got { len (files )} "
1346+ return files [0 ].read_bytes ()
1347+
1348+ # The Dash deserialize/roundtrip wrappers prepend a 4-byte stream version.
1349+ prefix = len (_stream_version_prefix ())
1350+ # CRecoveredSig: 1 + 32 + 32 + 32 + 96 = 193
1351+ assert len (_seed_bytes ("dash_recovered_sig_deserialize" )) == prefix + 193
1352+ # CSigSesAnn (VARINT(0)=1 byte): 1 + 1 + 32 + 32 + 32 = 98
1353+ assert len (_seed_bytes ("dash_sig_ses_ann_deserialize" )) == prefix + 98
1354+ # CSigShare: 1 + 32 + 2 + 32 + 32 + 96 = 195
1355+ assert len (_seed_bytes ("dash_sig_share_deserialize" )) == prefix + 195
1356+ # CDKGComplaint with empty bitsets: 1 + 32 + 32 + 1 + 1 + 96 = 163
1357+ assert len (_seed_bytes ("dash_dkg_complaint_deserialize" )) == prefix + 163
1358+
1359+ # --stream-version override path: setting the override must not require
1360+ # src/version.h, and _stream_version_prefix must round-trip the value.
1361+ global _STREAM_VERSION_OVERRIDE , _STREAM_VERSION_CACHE
1362+ saved_override , saved_cache = _STREAM_VERSION_OVERRIDE , _STREAM_VERSION_CACHE
1363+ try :
1364+ _STREAM_VERSION_OVERRIDE = 0x12345678
1365+ _STREAM_VERSION_CACHE = None
1366+ assert _stream_version_prefix () == b"\x78 \x56 \x34 \x12 "
1367+ finally :
1368+ _STREAM_VERSION_OVERRIDE = saved_override
1369+ _STREAM_VERSION_CACHE = saved_cache
1370+
12971371
12981372def main ():
12991373 parser = argparse .ArgumentParser (
@@ -1317,8 +1391,22 @@ def main():
13171391 action = "store_true" ,
13181392 help = "Only generate synthetic seeds (no RPC required)"
13191393 )
1394+ parser .add_argument (
1395+ "--stream-version" ,
1396+ type = int ,
1397+ default = None ,
1398+ help = (
1399+ "Stream version (4-byte LE prefix) to use for harnesses that consume one. "
1400+ "Overrides DASH_FUZZ_STREAM_VERSION and the src/version.h fallback. "
1401+ "Useful when running outside an in-tree source checkout."
1402+ ),
1403+ )
13201404 args = parser .parse_args ()
13211405
1406+ if args .stream_version is not None :
1407+ global _STREAM_VERSION_OVERRIDE
1408+ _STREAM_VERSION_OVERRIDE = args .stream_version
1409+
13221410 output_dir = Path (args .output_dir )
13231411 output_dir .mkdir (parents = True , exist_ok = True )
13241412
0 commit comments