From 77ea31158efb47d31dc07140df97f85518f074c0 Mon Sep 17 00:00:00 2001 From: bluezr Date: Sat, 1 Aug 2026 09:59:14 -0700 Subject: [PATCH 1/7] bip152: add compact block types, serialization and tests Message types and (de)serializers for compact block relay: sendcmpct, cmpctblock, getblocktxn and blocktxn, plus short-id derivation and the prefilled-transaction encoding. Dogecoin is pre-SegWit, so only BIP152 version 1 applies -- short ids are SipHash-2-4 over the txid, not the wtxid that version 2 introduced for witness serialization. CMPCTBLOCK_VERSION is 1 accordingly, and a peer announcing version 2 is not something this code can honour. Wire counts are bounded before they size an allocation. prefilled_count, indices_count and txs_count are compact_size values read straight off the wire, and feeding them to calloc() before reading any element reserves ~68 GB, ~17 GB and ~34 GB respectively at 0xFFFFFFFF -- a memory exhaustion DoS from a tiny message, the same class as the getheaders locator count. Every element occupies a minimum number of bytes on the wire, so any count the remaining buffer cannot hold is invalid by construction and is rejected before allocating. Nothing dispatches these messages yet; the P2P wiring follows separately so it can be reviewed on its own. 79/79 with test_compact_block registered and running. --- CMakeLists.txt | 1 + Makefile.am | 2 + include/dogecoin/compact_block.h | 386 ++++++++++++++++++ src/compact_block.c | 657 +++++++++++++++++++++++++++++++ test/compact_block_tests.c | 310 +++++++++++++++ test/unittester.c | 2 + 6 files changed, 1358 insertions(+) create mode 100644 include/dogecoin/compact_block.h create mode 100644 src/compact_block.c create mode 100644 test/compact_block_tests.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 72dc941aa..b0869042d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -685,6 +685,7 @@ IF(WITH_NET) ) TARGET_SOURCES(${LIBDOGECOIN_NAME} ${visibility} src/bip37.c + src/compact_block.c src/headersdb_file.c src/net.c src/protocol.c diff --git a/Makefile.am b/Makefile.am index 14dc6d8ca..d5f22d856 100644 --- a/Makefile.am +++ b/Makefile.am @@ -104,6 +104,7 @@ libdogecoin_la_SOURCES = \ src/bip39.c \ src/bip44.c \ src/block.c \ + src/compact_block.c \ src/buffer.c \ src/chacha20.c \ src/context.c \ @@ -278,6 +279,7 @@ tests_SOURCES = \ test/bip39_tests.c \ test/bip44_tests.c \ test/block_tests.c \ + test/compact_block_tests.c \ test/buffer_tests.c \ test/chacha20_tests.c \ test/context_tests.c \ diff --git a/include/dogecoin/compact_block.h b/include/dogecoin/compact_block.h new file mode 100644 index 000000000..0a0a2e735 --- /dev/null +++ b/include/dogecoin/compact_block.h @@ -0,0 +1,386 @@ +/* + + The MIT License (MIT) + + Copyright (c) 2016 Matt Corallo + Copyright (c) 2024 bluezr + Copyright (c) 2024-2026 The Dogecoin Foundation + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +/** + * @file compact_block.h + * @brief BIP152 Compact Block Relay implementation for Dogecoin. + * + * Implements Compact Blocks as specified in BIP152, enabling more + * efficient block propagation by sending compact representations + * containing short transaction IDs instead of full transactions. + * + * Message types: + * - sendcmpct: Negotiate compact block support with peers + * - cmpctblock: Compact block representation with short tx IDs + * - getblocktxn: Request specific missing transactions + * - blocktxn: Response with requested transactions + * + * Reference: https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki + */ + +#ifndef __LIBDOGECOIN_COMPACT_BLOCK_H__ +#define __LIBDOGECOIN_COMPACT_BLOCK_H__ + +#include +#include +#include +#include +#include +#include +#include +#include + +LIBDOGECOIN_BEGIN_DECL + +/* ================================================================ */ +/* BIP152 Constants */ +/* ================================================================ */ + +/** BIP152 compact block version (low-bandwidth relaying) */ +#define CMPCTBLOCK_VERSION 1 + +/** Short transaction ID length in bytes (6 bytes = 48 bits) */ +#define SHORTTXID_LENGTH 6 + +/* ================================================================ */ +/* BIP152 Data Structures */ +/* ================================================================ */ + +/** + * @brief A prefilled transaction in a compact block. + * + * Used for the coinbase and possibly other transactions that the + * sender expects the receiver to not already have in mempool. + * + * The index is differentially encoded in the serialized form. + */ +typedef struct dogecoin_prefilled_tx_ { + uint32_t index; /**< Original index of the transaction in the block */ + dogecoin_tx *tx; /**< The full transaction */ +} dogecoin_prefilled_tx; + +/** + * @brief BIP152 Compact Block header + short IDs. + * + * Contains the block header, a nonce for computing short IDs, + * the short transaction IDs for transactions the receiver likely + * has in their mempool, and pre-filled transactions (at minimum + * the coinbase). + */ +typedef struct dogecoin_compact_block_ { + dogecoin_block_header header; /**< Block header (80 bytes, no auxpow serialized) */ + uint64_t nonce; /**< Random nonce for SipHash key derivation */ + uint64_t sipkey_k0; /**< SipHash key 0 (derived from header + nonce) */ + uint64_t sipkey_k1; /**< SipHash key 1 (derived from header + nonce) */ + uint32_t short_ids_count; /**< Number of short transaction IDs */ + uint8_t *short_ids; /**< Array of 6-byte short IDs (short_ids_count * 6) */ + uint32_t prefilled_count; /**< Number of prefilled transactions */ + dogecoin_prefilled_tx *prefilled_txs; /**< Array of prefilled transactions */ +} dogecoin_compact_block; + +/** + * @brief BIP152 getblocktxn request. + * + * Sent by a node that received a compact block but is missing + * some transactions. Contains the block hash and the indices + * of requested transactions (differentially encoded). + */ +typedef struct dogecoin_getblocktxn_ { + uint256_t blockhash; /**< Hash of the compact block */ + uint32_t indices_count; /**< Number of requested tx indices */ + uint32_t *indices; /**< Array of requested tx indices (differentially encoded) */ +} dogecoin_getblocktxn; + +/** + * @brief BIP152 blocktxn response. + * + * Response to getblocktxn containing the requested full transactions. + */ +typedef struct dogecoin_blocktxn_ { + uint256_t blockhash; /**< Hash of the block */ + uint32_t txs_count; /**< Number of transactions */ + dogecoin_tx **txs; /**< Array of full transactions */ +} dogecoin_blocktxn; + +/** + * @brief Per-node compact block state. + * + * Tracks BIP152 negotiation state and pending compact blocks + * for a given peer. + */ +typedef struct dogecoin_compact_block_state_ { + dogecoin_bool compact_blocks_enabled; /**< Peer supports compact blocks */ + dogecoin_bool high_bandwidth_mode; /**< High-bandwidth mode requested */ + uint64_t compact_block_version; /**< Negotiated compact block version */ + + /* Pending compact block waiting for missing txs */ + dogecoin_compact_block *pending_cmpctblock; /**< Compact block awaiting completion */ + dogecoin_tx **available_txs; /**< Resolved transactions (NULL for missing) */ + uint32_t available_txs_count; /**< Total tx count (shortids + prefilled) */ + uint32_t *missing_indices; /**< Indices of missing transactions */ + uint32_t missing_count; /**< Number of missing transactions */ +} dogecoin_compact_block_state; + +/* ================================================================ */ +/* Constructor / Destructor */ +/* ================================================================ */ + +/** + * @brief Create a new compact block object. + * @return Allocated compact block, or NULL on failure. + */ +LIBDOGECOIN_API dogecoin_compact_block *dogecoin_compact_block_new(void); + +/** + * @brief Free a compact block object. + * @param cmpctblock The compact block to free. + */ +LIBDOGECOIN_API void dogecoin_compact_block_free(dogecoin_compact_block *cmpctblock); + +/** + * @brief Create a new getblocktxn request. + * @return Allocated getblocktxn, or NULL on failure. + */ +LIBDOGECOIN_API dogecoin_getblocktxn *dogecoin_getblocktxn_new(void); + +/** + * @brief Free a getblocktxn request. + * @param req The request to free. + */ +LIBDOGECOIN_API void dogecoin_getblocktxn_free(dogecoin_getblocktxn *req); + +/** + * @brief Create a new blocktxn response. + * @return Allocated blocktxn, or NULL on failure. + */ +LIBDOGECOIN_API dogecoin_blocktxn *dogecoin_blocktxn_new(void); + +/** + * @brief Free a blocktxn response. + * @param resp The response to free. + */ +LIBDOGECOIN_API void dogecoin_blocktxn_free(dogecoin_blocktxn *resp); + +/** + * @brief Create a new per-node compact block state. + * @return Allocated state, or NULL on failure. + */ +LIBDOGECOIN_API dogecoin_compact_block_state *dogecoin_compact_block_state_new(void); + +/** + * @brief Free a per-node compact block state. + * @param state The state to free. + */ +LIBDOGECOIN_API void dogecoin_compact_block_state_free(dogecoin_compact_block_state *state); + +/* ================================================================ */ +/* Short Transaction ID Computation */ +/* ================================================================ */ + +/** + * @brief Derive the SipHash keys from a block header and nonce. + * + * Per BIP152: SHA256(block_header || nonce) produces k0 and k1 as + * the first two little-endian 64-bit integers of the hash. + * + * @param header Serialized block header (80 bytes). + * @param nonce The compact block nonce. + * @param k0_out Output: SipHash key 0. + * @param k1_out Output: SipHash key 1. + */ +LIBDOGECOIN_API void dogecoin_compact_block_derive_sipkeys( + const dogecoin_block_header *header, + uint64_t nonce, + uint64_t *k0_out, + uint64_t *k1_out); + +/** + * @brief Compute a 6-byte short transaction ID. + * + * Per BIP152: SipHash-2-4(k0, k1, txid) truncated to 6 bytes (LE). + * + * @param k0 SipHash key 0. + * @param k1 SipHash key 1. + * @param txhash The transaction hash (txid). + * @param short_id Output: 6-byte short ID. + */ +LIBDOGECOIN_API void dogecoin_compact_block_compute_short_id( + uint64_t k0, + uint64_t k1, + const uint256_t txhash, + uint8_t short_id[SHORTTXID_LENGTH]); + +/* ================================================================ */ +/* Serialization / Deserialization */ +/* ================================================================ */ + +/** + * @brief Serialize a compact block to a cstring. + * @param s Output cstring. + * @param cmpctblk The compact block to serialize. + */ +LIBDOGECOIN_API void dogecoin_compact_block_serialize(cstring *s, const dogecoin_compact_block *cmpctblk); + +/** + * @brief Deserialize a compact block from a buffer. + * @param cmpctblk Output compact block. + * @param buf Input buffer. + * @param params Chain parameters (for header deserialization). + * @return true on success, false on failure. + */ +LIBDOGECOIN_API dogecoin_bool dogecoin_compact_block_deserialize( + dogecoin_compact_block *cmpctblk, + struct const_buffer *buf, + const dogecoin_chainparams *params); + +/** + * @brief Serialize a getblocktxn request. + * @param s Output cstring. + * @param req The request to serialize. + */ +LIBDOGECOIN_API void dogecoin_getblocktxn_serialize(cstring *s, const dogecoin_getblocktxn *req); + +/** + * @brief Deserialize a getblocktxn request from a buffer. + * @param req Output request. + * @param buf Input buffer. + * @return true on success. + */ +LIBDOGECOIN_API dogecoin_bool dogecoin_getblocktxn_deserialize( + dogecoin_getblocktxn *req, + struct const_buffer *buf); + +/** + * @brief Serialize a blocktxn response. + * @param s Output cstring. + * @param resp The response to serialize. + */ +LIBDOGECOIN_API void dogecoin_blocktxn_serialize(cstring *s, const dogecoin_blocktxn *resp); + +/** + * @brief Deserialize a blocktxn response from a buffer. + * @param resp Output response. + * @param buf Input buffer. + * @return true on success. + */ +LIBDOGECOIN_API dogecoin_bool dogecoin_blocktxn_deserialize( + dogecoin_blocktxn *resp, + struct const_buffer *buf); + +/* ================================================================ */ +/* P2P Message Construction (requires WITH_NET) */ +/* ================================================================ */ + +#ifdef WITH_NET + +#include + +/** + * @brief Build a sendcmpct P2P message. + * + * Announces compact block support to a peer. + * + * @param netmagic Network magic bytes. + * @param high_bandwidth true = request high-bandwidth mode. + * @param version Compact block version (1 for BIP152 v1). + * @return P2P message cstring, or NULL on failure. Caller frees. + */ +LIBDOGECOIN_API cstring *dogecoin_p2p_msg_sendcmpct( + const unsigned char netmagic[4], + dogecoin_bool high_bandwidth, + uint64_t version); + +/** + * @brief Build a cmpctblock P2P message. + * @param netmagic Network magic bytes. + * @param cmpctblk The compact block to send. + * @return P2P message cstring. Caller frees. + */ +LIBDOGECOIN_API cstring *dogecoin_p2p_msg_cmpctblock( + const unsigned char netmagic[4], + const dogecoin_compact_block *cmpctblk); + +/** + * @brief Build a getblocktxn P2P message. + * @param netmagic Network magic bytes. + * @param req The getblocktxn request. + * @return P2P message cstring. Caller frees. + */ +LIBDOGECOIN_API cstring *dogecoin_p2p_msg_getblocktxn( + const unsigned char netmagic[4], + const dogecoin_getblocktxn *req); + +/** + * @brief Build a blocktxn P2P message. + * @param netmagic Network magic bytes. + * @param resp The blocktxn response. + * @return P2P message cstring. Caller frees. + */ +LIBDOGECOIN_API cstring *dogecoin_p2p_msg_blocktxn( + const unsigned char netmagic[4], + const dogecoin_blocktxn *resp); + +#endif /* WITH_NET */ + +/* ================================================================ */ +/* Compact Block Processing */ +/* ================================================================ */ + +/** + * @brief Attempt to reconstruct a full block from a compact block + * and known transactions (e.g., from mempool). + * + * Fills state->available_txs with resolved transactions and sets + * state->missing_indices / missing_count for any that are unknown. + * + * @param cmpctblk The received compact block. + * @param state Per-node compact block state (output). + * @param known_txs Array of known transactions. + * @param known_txs_count Number of known transactions. + * @return true if all transactions were resolved (block is complete). + */ +LIBDOGECOIN_API dogecoin_bool dogecoin_compact_block_reconstruct( + const dogecoin_compact_block *cmpctblk, + dogecoin_compact_block_state *state, + dogecoin_tx **known_txs, + uint32_t known_txs_count); + +/** + * @brief Fill in missing transactions from a blocktxn response. + * + * @param state Per-node compact block state (updated). + * @param resp The blocktxn response with the missing txs. + * @return true if the block is now fully reconstructed. + */ +LIBDOGECOIN_API dogecoin_bool dogecoin_compact_block_fill_missing( + dogecoin_compact_block_state *state, + const dogecoin_blocktxn *resp); + +LIBDOGECOIN_END_DECL + +#endif /* __LIBDOGECOIN_COMPACT_BLOCK_H__ */ diff --git a/src/compact_block.c b/src/compact_block.c new file mode 100644 index 000000000..9209e261e --- /dev/null +++ b/src/compact_block.c @@ -0,0 +1,657 @@ +/* + + The MIT License (MIT) + + Copyright (c) 2016 Matt Corallo + Copyright (c) 2024 bluezr + Copyright (c) 2024-2026 The Dogecoin Foundation + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +/** + * @file compact_block.c + * @brief BIP152 Compact Block Relay implementation. + * + * Implements the data structures, serialization, short transaction ID + * computation, and P2P message construction for BIP152 compact blocks. + */ + +#include + +#include +#include +#include +#include +#include + +/* ================================================================ */ +/* Constructor / Destructor */ +/* ================================================================ */ + +dogecoin_compact_block *dogecoin_compact_block_new(void) +{ + dogecoin_compact_block *cmpctblk = dogecoin_calloc(1, sizeof(*cmpctblk)); + if (!cmpctblk) return NULL; + return cmpctblk; +} + +void dogecoin_compact_block_free(dogecoin_compact_block *cmpctblk) +{ + if (!cmpctblk) return; + + if (cmpctblk->short_ids) { + dogecoin_free(cmpctblk->short_ids); + cmpctblk->short_ids = NULL; + } + + if (cmpctblk->prefilled_txs) { + uint32_t i; + for (i = 0; i < cmpctblk->prefilled_count; i++) { + if (cmpctblk->prefilled_txs[i].tx) { + dogecoin_tx_free(cmpctblk->prefilled_txs[i].tx); + } + } + dogecoin_free(cmpctblk->prefilled_txs); + cmpctblk->prefilled_txs = NULL; + } + + dogecoin_free(cmpctblk); +} + +dogecoin_getblocktxn *dogecoin_getblocktxn_new(void) +{ + dogecoin_getblocktxn *req = dogecoin_calloc(1, sizeof(*req)); + if (!req) return NULL; + return req; +} + +void dogecoin_getblocktxn_free(dogecoin_getblocktxn *req) +{ + if (!req) return; + if (req->indices) { + dogecoin_free(req->indices); + req->indices = NULL; + } + dogecoin_free(req); +} + +dogecoin_blocktxn *dogecoin_blocktxn_new(void) +{ + dogecoin_blocktxn *resp = dogecoin_calloc(1, sizeof(*resp)); + if (!resp) return NULL; + return resp; +} + +void dogecoin_blocktxn_free(dogecoin_blocktxn *resp) +{ + if (!resp) return; + if (resp->txs) { + uint32_t i; + for (i = 0; i < resp->txs_count; i++) { + if (resp->txs[i]) { + dogecoin_tx_free(resp->txs[i]); + } + } + dogecoin_free(resp->txs); + resp->txs = NULL; + } + dogecoin_free(resp); +} + +dogecoin_compact_block_state *dogecoin_compact_block_state_new(void) +{ + dogecoin_compact_block_state *state = dogecoin_calloc(1, sizeof(*state)); + if (!state) return NULL; + state->compact_blocks_enabled = false; + state->high_bandwidth_mode = false; + state->compact_block_version = 0; + return state; +} + +void dogecoin_compact_block_state_free(dogecoin_compact_block_state *state) +{ + if (!state) return; + + if (state->pending_cmpctblock) { + dogecoin_compact_block_free(state->pending_cmpctblock); + state->pending_cmpctblock = NULL; + } + + if (state->available_txs) { + /* Note: we don't free the tx objects themselves here; they are + * borrowed references from mempool or prefilled txs. The compact + * block (which owns prefilled txs) handles freeing those. */ + dogecoin_free(state->available_txs); + state->available_txs = NULL; + } + + if (state->missing_indices) { + dogecoin_free(state->missing_indices); + state->missing_indices = NULL; + } + + dogecoin_free(state); +} + +/* ================================================================ */ +/* Short Transaction ID Computation */ +/* ================================================================ */ + +/** + * Per BIP152: The SipHash keys are derived from the SHA256 of + * the serialized block header (80 bytes) concatenated with the + * compact block nonce (8 bytes little-endian). + * + * k0 = first 8 bytes of SHA256 result (little-endian uint64) + * k1 = next 8 bytes of SHA256 result (little-endian uint64) + */ +void dogecoin_compact_block_derive_sipkeys( + const dogecoin_block_header *header, + uint64_t nonce, + uint64_t *k0_out, + uint64_t *k1_out) +{ + /* Serialize the 80-byte block header */ + cstring *hdr_ser = cstr_new_sz(88); + dogecoin_block_header_serialize(hdr_ser, header); + + /* Append the 8-byte nonce in little-endian */ + ser_u64(hdr_ser, nonce); + + /* SHA256(header || nonce) – single SHA256 per BIP152 */ + uint256_t hash; + sha256_raw((const uint8_t *)hdr_ser->str, hdr_ser->len, hash); + cstr_free(hdr_ser, true); + + /* Extract k0, k1 as little-endian uint64 from the hash */ + const uint8_t *p = hash; + *k0_out = ((uint64_t)p[0]) | ((uint64_t)p[1] << 8) | + ((uint64_t)p[2] << 16) | ((uint64_t)p[3] << 24) | + ((uint64_t)p[4] << 32) | ((uint64_t)p[5] << 40) | + ((uint64_t)p[6] << 48) | ((uint64_t)p[7] << 56); + + *k1_out = ((uint64_t)p[8]) | ((uint64_t)p[9] << 8) | + ((uint64_t)p[10] << 16) | ((uint64_t)p[11] << 24) | + ((uint64_t)p[12] << 32) | ((uint64_t)p[13] << 40) | + ((uint64_t)p[14] << 48) | ((uint64_t)p[15] << 56); +} + +/** + * Per BIP152: ShortTxID = SipHash-2-4(k0, k1, txid) & 0xFFFFFFFFFFFF + * The result is the lower 6 bytes in little-endian. + */ +void dogecoin_compact_block_compute_short_id( + uint64_t k0, + uint64_t k1, + const uint256_t txhash, + uint8_t short_id[SHORTTXID_LENGTH]) +{ + uint64_t siphash_result = siphash_u256(k0, k1, (uint256_t *)txhash); + + /* Take the lower 6 bytes (little-endian) */ + short_id[0] = (uint8_t)(siphash_result & 0xFF); + short_id[1] = (uint8_t)((siphash_result >> 8) & 0xFF); + short_id[2] = (uint8_t)((siphash_result >> 16) & 0xFF); + short_id[3] = (uint8_t)((siphash_result >> 24) & 0xFF); + short_id[4] = (uint8_t)((siphash_result >> 32) & 0xFF); + short_id[5] = (uint8_t)((siphash_result >> 40) & 0xFF); +} + +/* ================================================================ */ +/* Serialization */ +/* ================================================================ */ + +void dogecoin_compact_block_serialize(cstring *s, const dogecoin_compact_block *cmpctblk) +{ + if (!s || !cmpctblk) return; + + /* Block header (80 bytes) */ + dogecoin_block_header_serialize(s, &cmpctblk->header); + + /* Nonce (8 bytes LE) */ + ser_u64(s, cmpctblk->nonce); + + /* Short IDs: varint count + raw 6-byte IDs */ + ser_varlen(s, cmpctblk->short_ids_count); + if (cmpctblk->short_ids_count > 0 && cmpctblk->short_ids) { + ser_bytes(s, cmpctblk->short_ids, + (size_t)cmpctblk->short_ids_count * SHORTTXID_LENGTH); + } + + /* Prefilled transactions: varint count + differentially encoded entries */ + ser_varlen(s, cmpctblk->prefilled_count); + uint32_t last_index = 0; + uint32_t i; + for (i = 0; i < cmpctblk->prefilled_count; i++) { + const dogecoin_prefilled_tx *ptx = &cmpctblk->prefilled_txs[i]; + /* Differential encoding: encode (index - last_index) for first, + * (index - last_index - 1) for subsequent */ + uint32_t diff = (i == 0) ? ptx->index : (ptx->index - last_index - 1); + ser_varlen(s, diff); + dogecoin_tx_serialize(s, ptx->tx); + last_index = ptx->index; + } +} + +dogecoin_bool dogecoin_compact_block_deserialize( + dogecoin_compact_block *cmpctblk, + struct const_buffer *buf, + const dogecoin_chainparams *params) +{ + if (!cmpctblk || !buf) return false; + + /* Deserialize block header (80 bytes, no auxpow for compact blocks) */ + if (!dogecoin_block_header_deserialize(&cmpctblk->header, buf, params, NULL)) + return false; + + /* Nonce */ + if (!deser_u64(&cmpctblk->nonce, buf)) + return false; + + /* Derive SipHash keys */ + dogecoin_compact_block_derive_sipkeys(&cmpctblk->header, cmpctblk->nonce, + &cmpctblk->sipkey_k0, &cmpctblk->sipkey_k1); + + /* Short IDs */ + if (!deser_varlen(&cmpctblk->short_ids_count, buf)) + return false; + + if (cmpctblk->short_ids_count > 0) { + size_t ids_bytes = (size_t)cmpctblk->short_ids_count * SHORTTXID_LENGTH; + if (buf->len < ids_bytes) + return false; + cmpctblk->short_ids = dogecoin_calloc(1, ids_bytes); + if (!cmpctblk->short_ids) + return false; + if (!deser_bytes(cmpctblk->short_ids, buf, ids_bytes)) { + dogecoin_free(cmpctblk->short_ids); + cmpctblk->short_ids = NULL; + return false; + } + } + + /* Prefilled transactions */ + if (!deser_varlen(&cmpctblk->prefilled_count, buf)) + return false; + + if (cmpctblk->prefilled_count > 0) { + /* prefilled_count is attacker-controlled. Each prefilled entry costs at + least a varint index plus a minimal transaction on the wire, so a count + exceeding the remaining buffer length cannot be honest; without this the + calloc below reserves prefilled_count * sizeof(dogecoin_prefilled_tx) + (~68 GB at 0xFFFFFFFF) before anything is parsed. The short_ids branch + above already bounds itself this way. */ + if (cmpctblk->prefilled_count > buf->len) return false; + cmpctblk->prefilled_txs = dogecoin_calloc(cmpctblk->prefilled_count, + sizeof(dogecoin_prefilled_tx)); + if (!cmpctblk->prefilled_txs) + return false; + + uint32_t last_index = 0; + uint32_t i; + for (i = 0; i < cmpctblk->prefilled_count; i++) { + uint32_t diff; + if (!deser_varlen(&diff, buf)) + return false; + + /* Undo differential encoding */ + if (i == 0) { + cmpctblk->prefilled_txs[i].index = diff; + } else { + cmpctblk->prefilled_txs[i].index = last_index + diff + 1; + } + last_index = cmpctblk->prefilled_txs[i].index; + + /* Deserialize the full transaction */ + cmpctblk->prefilled_txs[i].tx = dogecoin_tx_new(); + size_t consumed = 0; + if (!dogecoin_tx_deserialize(buf->p, buf->len, + cmpctblk->prefilled_txs[i].tx, &consumed)) { + return false; + } + if (!deser_skip(buf, consumed)) + return false; + } + } + + return true; +} + +void dogecoin_getblocktxn_serialize(cstring *s, const dogecoin_getblocktxn *req) +{ + if (!s || !req) return; + + /* Block hash */ + ser_u256(s, req->blockhash); + + /* Indices (differentially encoded) */ + ser_varlen(s, req->indices_count); + uint32_t last_index = 0; + uint32_t i; + for (i = 0; i < req->indices_count; i++) { + uint32_t diff = (i == 0) ? req->indices[i] : (req->indices[i] - last_index - 1); + ser_varlen(s, diff); + last_index = req->indices[i]; + } +} + +dogecoin_bool dogecoin_getblocktxn_deserialize( + dogecoin_getblocktxn *req, + struct const_buffer *buf) +{ + if (!req || !buf) return false; + + if (!deser_u256(req->blockhash, buf)) + return false; + + if (!deser_varlen(&req->indices_count, buf)) + return false; + + if (req->indices_count > 0) { + /* Each index is a differentially-encoded varint, so it occupies at least one + byte on the wire: a count larger than the remaining buffer is impossible. + Unbounded, the calloc below reserves indices_count * 4 bytes (~17 GB at + 0xFFFFFFFF) from a request message only 33 bytes long. */ + if (req->indices_count > buf->len) return false; + req->indices = dogecoin_calloc(req->indices_count, sizeof(uint32_t)); + if (!req->indices) + return false; + + uint32_t last_index = 0; + uint32_t i; + for (i = 0; i < req->indices_count; i++) { + uint32_t diff; + if (!deser_varlen(&diff, buf)) + return false; + if (i == 0) { + req->indices[i] = diff; + } else { + req->indices[i] = last_index + diff + 1; + } + last_index = req->indices[i]; + } + } + + return true; +} + +void dogecoin_blocktxn_serialize(cstring *s, const dogecoin_blocktxn *resp) +{ + if (!s || !resp) return; + + ser_u256(s, resp->blockhash); + ser_varlen(s, resp->txs_count); + uint32_t i; + for (i = 0; i < resp->txs_count; i++) { + if (resp->txs[i]) { + dogecoin_tx_serialize(s, resp->txs[i]); + } + } +} + +dogecoin_bool dogecoin_blocktxn_deserialize( + dogecoin_blocktxn *resp, + struct const_buffer *buf) +{ + if (!resp || !buf) return false; + + if (!deser_u256(resp->blockhash, buf)) + return false; + + if (!deser_varlen(&resp->txs_count, buf)) + return false; + + if (resp->txs_count > 0) { + /* Each transaction that follows is several bytes minimum, so a count beyond + the remaining buffer cannot be satisfied. Unbounded, the calloc below + reserves txs_count pointers (~34 GB at 0xFFFFFFFF) before the first + transaction is deserialized. */ + if (resp->txs_count > buf->len) return false; + resp->txs = dogecoin_calloc(resp->txs_count, sizeof(dogecoin_tx *)); + if (!resp->txs) + return false; + + uint32_t i; + for (i = 0; i < resp->txs_count; i++) { + resp->txs[i] = dogecoin_tx_new(); + size_t consumed = 0; + if (!dogecoin_tx_deserialize(buf->p, buf->len, + resp->txs[i], &consumed)) { + return false; + } + if (!deser_skip(buf, consumed)) + return false; + } + } + + return true; +} + +/* ================================================================ */ +/* P2P Message Construction */ +/* ================================================================ */ + +#ifdef WITH_NET + +cstring *dogecoin_p2p_msg_sendcmpct( + const unsigned char netmagic[4], + dogecoin_bool high_bandwidth, + uint64_t version) +{ + cstring *payload = cstr_new_sz(9); + /* fAnnounce: 1 byte boolean */ + uint8_t announce = high_bandwidth ? 1 : 0; + ser_bytes(payload, &announce, 1); + /* nCmpctVersion: 8 bytes LE */ + ser_u64(payload, version); + + cstring *msg = dogecoin_p2p_message_new(netmagic, DOGECOIN_MSG_SENDCMPCT, + payload->str, payload->len); + cstr_free(payload, true); + return msg; +} + +cstring *dogecoin_p2p_msg_cmpctblock( + const unsigned char netmagic[4], + const dogecoin_compact_block *cmpctblk) +{ + if (!cmpctblk) return NULL; + + cstring *payload = cstr_new_sz(512); + dogecoin_compact_block_serialize(payload, cmpctblk); + + cstring *msg = dogecoin_p2p_message_new(netmagic, DOGECOIN_MSG_CMPCTBLOCK, + payload->str, payload->len); + cstr_free(payload, true); + return msg; +} + +cstring *dogecoin_p2p_msg_getblocktxn( + const unsigned char netmagic[4], + const dogecoin_getblocktxn *req) +{ + if (!req) return NULL; + + cstring *payload = cstr_new_sz(64); + dogecoin_getblocktxn_serialize(payload, req); + + cstring *msg = dogecoin_p2p_message_new(netmagic, DOGECOIN_MSG_GETBLOCKTXN, + payload->str, payload->len); + cstr_free(payload, true); + return msg; +} + +cstring *dogecoin_p2p_msg_blocktxn( + const unsigned char netmagic[4], + const dogecoin_blocktxn *resp) +{ + if (!resp) return NULL; + + cstring *payload = cstr_new_sz(256); + dogecoin_blocktxn_serialize(payload, resp); + + cstring *msg = dogecoin_p2p_message_new(netmagic, DOGECOIN_MSG_BLOCKTXN, + payload->str, payload->len); + cstr_free(payload, true); + return msg; +} + +#endif /* WITH_NET */ + +/* ================================================================ */ +/* Compact Block Reconstruction */ +/* ================================================================ */ + +/** + * Compare two 6-byte short IDs. + * Returns 0 if equal, non-zero otherwise. + */ +static int shortid_cmp(const uint8_t *a, const uint8_t *b) +{ + return memcmp(a, b, SHORTTXID_LENGTH); +} + +dogecoin_bool dogecoin_compact_block_reconstruct( + const dogecoin_compact_block *cmpctblk, + dogecoin_compact_block_state *state, + dogecoin_tx **known_txs, + uint32_t known_txs_count) +{ + if (!cmpctblk || !state) return false; + + /* Total transaction count = short_ids + prefilled */ + uint32_t total_txs = cmpctblk->short_ids_count + cmpctblk->prefilled_count; + if (total_txs == 0) return false; + + /* Allocate the available_txs array (NULL means missing) */ + state->available_txs = dogecoin_calloc(total_txs, sizeof(dogecoin_tx *)); + if (!state->available_txs) return false; + state->available_txs_count = total_txs; + + /* Place prefilled transactions at their correct indices */ + uint32_t i; + for (i = 0; i < cmpctblk->prefilled_count; i++) { + uint32_t idx = cmpctblk->prefilled_txs[i].index; + if (idx >= total_txs) { + /* Invalid index */ + dogecoin_free(state->available_txs); + state->available_txs = NULL; + return false; + } + state->available_txs[idx] = cmpctblk->prefilled_txs[i].tx; + } + + /* Build a mapping from short_ids positions to available_txs positions. + * short_ids[j] corresponds to the j-th non-prefilled slot. */ + uint32_t short_idx = 0; + uint32_t *shortid_to_txpos = dogecoin_calloc(cmpctblk->short_ids_count, sizeof(uint32_t)); + if (!shortid_to_txpos && cmpctblk->short_ids_count > 0) return false; + + for (i = 0; i < total_txs && short_idx < cmpctblk->short_ids_count; i++) { + if (state->available_txs[i] == NULL) { + shortid_to_txpos[short_idx] = i; + short_idx++; + } + } + + /* Try to match each short ID against known transactions */ + uint32_t missing_count = 0; + uint32_t j; + for (j = 0; j < cmpctblk->short_ids_count; j++) { + const uint8_t *target_shortid = &cmpctblk->short_ids[j * SHORTTXID_LENGTH]; + dogecoin_bool found = false; + + uint32_t k; + for (k = 0; k < known_txs_count; k++) { + if (!known_txs[k]) continue; + + /* Compute the short ID for this known transaction */ + uint256_t txhash; + dogecoin_tx_hash(known_txs[k], txhash); + + uint8_t computed_shortid[SHORTTXID_LENGTH]; + dogecoin_compact_block_compute_short_id( + cmpctblk->sipkey_k0, cmpctblk->sipkey_k1, + txhash, computed_shortid); + + if (shortid_cmp(target_shortid, computed_shortid) == 0) { + state->available_txs[shortid_to_txpos[j]] = known_txs[k]; + found = true; + break; + } + } + + if (!found) { + missing_count++; + } + } + + /* Build the missing indices list */ + if (missing_count > 0) { + state->missing_indices = dogecoin_calloc(missing_count, sizeof(uint32_t)); + if (!state->missing_indices) { + dogecoin_free(shortid_to_txpos); + return false; + } + state->missing_count = missing_count; + + uint32_t mi = 0; + for (j = 0; j < cmpctblk->short_ids_count; j++) { + if (state->available_txs[shortid_to_txpos[j]] == NULL) { + state->missing_indices[mi++] = shortid_to_txpos[j]; + } + } + } else { + state->missing_count = 0; + state->missing_indices = NULL; + } + + dogecoin_free(shortid_to_txpos); + + return (missing_count == 0); +} + +dogecoin_bool dogecoin_compact_block_fill_missing( + dogecoin_compact_block_state *state, + const dogecoin_blocktxn *resp) +{ + if (!state || !resp) return false; + + if (resp->txs_count != state->missing_count) { + /* Mismatch between requested and received transaction count */ + return false; + } + + uint32_t i; + for (i = 0; i < resp->txs_count; i++) { + uint32_t idx = state->missing_indices[i]; + if (idx >= state->available_txs_count) return false; + state->available_txs[idx] = resp->txs[i]; + } + + /* Verify all slots are filled */ + for (i = 0; i < state->available_txs_count; i++) { + if (state->available_txs[i] == NULL) return false; + } + + state->missing_count = 0; + return true; +} diff --git a/test/compact_block_tests.c b/test/compact_block_tests.c new file mode 100644 index 000000000..2ae1b1fc6 --- /dev/null +++ b/test/compact_block_tests.c @@ -0,0 +1,310 @@ +/* + + The MIT License (MIT) + + Copyright (c) 2026 bluezr + Copyright (c) 2026 The Dogecoin Foundation + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include + +#include + +#include +#include +#include +#include + +/* ================================================================ */ +/* Lifecycle */ +/* ================================================================ */ + +static void test_compact_block_new_free(void) +{ + dogecoin_compact_block *cb = dogecoin_compact_block_new(); + u_assert_not_null(cb); + u_assert_is_null(cb->short_ids); + u_assert_uint32_eq(cb->short_ids_count, 0); + u_assert_is_null(cb->prefilled_txs); + u_assert_uint32_eq(cb->prefilled_count, 0); + u_assert_uint64_eq(cb->nonce, 0); + dogecoin_compact_block_free(cb); +} + +static void test_getblocktxn_new_free(void) +{ + dogecoin_getblocktxn *req = dogecoin_getblocktxn_new(); + u_assert_not_null(req); + u_assert_is_null(req->indices); + u_assert_uint32_eq(req->indices_count, 0); + dogecoin_getblocktxn_free(req); +} + +static void test_blocktxn_new_free(void) +{ + dogecoin_blocktxn *resp = dogecoin_blocktxn_new(); + u_assert_not_null(resp); + u_assert_is_null(resp->txs); + u_assert_uint32_eq(resp->txs_count, 0); + dogecoin_blocktxn_free(resp); +} + +static void test_compact_block_state_new_free(void) +{ + dogecoin_compact_block_state *state = dogecoin_compact_block_state_new(); + u_assert_not_null(state); + u_assert_true(!state->compact_blocks_enabled); + u_assert_true(!state->high_bandwidth_mode); + u_assert_uint64_eq(state->compact_block_version, 0); + u_assert_is_null(state->pending_cmpctblock); + u_assert_is_null(state->available_txs); + u_assert_is_null(state->missing_indices); + u_assert_uint32_eq(state->missing_count, 0); + dogecoin_compact_block_state_free(state); +} + +/* ================================================================ */ +/* SipHash key derivation */ +/* ================================================================ */ + +static void test_sipkeys_deterministic(void) +{ + dogecoin_block_header hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.version = 1; + hdr.bits = 0x1e0ffff0; + hdr.timestamp = 1386325540; + + uint64_t k0a, k1a, k0b, k1b; + dogecoin_compact_block_derive_sipkeys(&hdr, 12345ULL, &k0a, &k1a); + dogecoin_compact_block_derive_sipkeys(&hdr, 12345ULL, &k0b, &k1b); + + /* Same inputs → same keys */ + u_assert_uint64_eq(k0a, k0b); + u_assert_uint64_eq(k1a, k1b); + + /* Different nonce → different keys */ + uint64_t k0c, k1c; + dogecoin_compact_block_derive_sipkeys(&hdr, 99999ULL, &k0c, &k1c); + u_assert_true(k0a != k0c || k1a != k1c); +} + +static void test_sipkeys_different_headers(void) +{ + dogecoin_block_header h1, h2; + memset(&h1, 0, sizeof(h1)); + memset(&h2, 0, sizeof(h2)); + h1.version = 1; + h2.version = 2; /* Only version differs */ + + uint64_t k0a, k1a, k0b, k1b; + dogecoin_compact_block_derive_sipkeys(&h1, 0, &k0a, &k1a); + dogecoin_compact_block_derive_sipkeys(&h2, 0, &k0b, &k1b); + + /* Different headers must produce different SipHash keys */ + u_assert_true(k0a != k0b || k1a != k1b); +} + +/* ================================================================ */ +/* Short ID computation */ +/* ================================================================ */ + +static void test_short_id_deterministic(void) +{ + uint64_t k0 = 0x0102030405060708ULL; + uint64_t k1 = 0x090A0B0C0D0E0F10ULL; + uint256_t txhash; + memset(txhash, 0x42, 32); + + uint8_t sid1[SHORTTXID_LENGTH]; + uint8_t sid2[SHORTTXID_LENGTH]; + + dogecoin_compact_block_compute_short_id(k0, k1, txhash, sid1); + dogecoin_compact_block_compute_short_id(k0, k1, txhash, sid2); + + /* Deterministic: same inputs → same output */ + u_assert_mem_eq(sid1, sid2, SHORTTXID_LENGTH); +} + +static void test_short_id_sensitivity(void) +{ + uint64_t k0 = 0xDEADBEEF00000000ULL; + uint64_t k1 = 0xCAFEBABE00000000ULL; + + uint256_t txhash1, txhash2; + memset(txhash1, 0x11, 32); + memset(txhash2, 0x22, 32); /* Only txhash differs */ + + uint8_t sid1[SHORTTXID_LENGTH]; + uint8_t sid2[SHORTTXID_LENGTH]; + + dogecoin_compact_block_compute_short_id(k0, k1, txhash1, sid1); + dogecoin_compact_block_compute_short_id(k0, k1, txhash2, sid2); + + /* Different txhash → different short ID (SipHash avalanche) */ + u_assert_mem_not_eq(sid1, sid2, SHORTTXID_LENGTH); + + /* Changing k0/k1 also changes the short ID */ + uint8_t sid3[SHORTTXID_LENGTH]; + dogecoin_compact_block_compute_short_id(k0 ^ 1, k1, txhash1, sid3); + u_assert_mem_not_eq(sid1, sid3, SHORTTXID_LENGTH); +} + +/* ================================================================ */ +/* getblocktxn serialization round-trip */ +/* ================================================================ */ + +static void test_getblocktxn_ser_deser(void) +{ + dogecoin_getblocktxn req; + memset(&req, 0, sizeof(req)); + memset(req.blockhash, 0xAA, 32); + req.indices_count = 4; + req.indices = dogecoin_calloc(4, sizeof(uint32_t)); + req.indices[0] = 0; /* differential: 0 */ + req.indices[1] = 3; /* differential: 3 - 0 - 1 = 2 */ + req.indices[2] = 5; /* differential: 5 - 3 - 1 = 1 */ + req.indices[3] = 10; /* differential: 10 - 5 - 1 = 4 */ + + cstring *buf = cstr_new_sz(64); + dogecoin_getblocktxn_serialize(buf, &req); + u_assert_true(buf->len > 0); + + dogecoin_getblocktxn req2; + memset(&req2, 0, sizeof(req2)); + struct const_buffer cbuf = {(const uint8_t *)buf->str, buf->len}; + u_assert_true(dogecoin_getblocktxn_deserialize(&req2, &cbuf)); + + u_assert_mem_eq(req2.blockhash, req.blockhash, 32); + u_assert_uint32_eq(req2.indices_count, 4); + u_assert_uint32_eq(req2.indices[0], 0); + u_assert_uint32_eq(req2.indices[1], 3); + u_assert_uint32_eq(req2.indices[2], 5); + u_assert_uint32_eq(req2.indices[3], 10); + u_assert_uint32_eq(cbuf.len, 0); /* fully consumed */ + + dogecoin_free(req.indices); + dogecoin_free(req2.indices); + cstr_free(buf, true); +} + +static void test_getblocktxn_single_index(void) +{ + dogecoin_getblocktxn req; + memset(&req, 0, sizeof(req)); + memset(req.blockhash, 0x55, 32); + req.indices_count = 1; + req.indices = dogecoin_calloc(1, sizeof(uint32_t)); + req.indices[0] = 7; + + cstring *buf = cstr_new_sz(64); + dogecoin_getblocktxn_serialize(buf, &req); + + dogecoin_getblocktxn req2; + memset(&req2, 0, sizeof(req2)); + struct const_buffer cbuf = {(const uint8_t *)buf->str, buf->len}; + u_assert_true(dogecoin_getblocktxn_deserialize(&req2, &cbuf)); + u_assert_uint32_eq(req2.indices_count, 1); + u_assert_uint32_eq(req2.indices[0], 7); + + dogecoin_free(req.indices); + dogecoin_free(req2.indices); + cstr_free(buf, true); +} + +static void test_getblocktxn_zero_indices(void) +{ + dogecoin_getblocktxn req; + memset(&req, 0, sizeof(req)); + memset(req.blockhash, 0x33, 32); + req.indices_count = 0; + req.indices = NULL; + + cstring *buf = cstr_new_sz(64); + dogecoin_getblocktxn_serialize(buf, &req); + + dogecoin_getblocktxn req2; + memset(&req2, 0, sizeof(req2)); + struct const_buffer cbuf = {(const uint8_t *)buf->str, buf->len}; + u_assert_true(dogecoin_getblocktxn_deserialize(&req2, &cbuf)); + u_assert_mem_eq(req2.blockhash, req.blockhash, 32); + u_assert_uint32_eq(req2.indices_count, 0); + u_assert_is_null(req2.indices); + + cstr_free(buf, true); +} + +/* ================================================================ */ +/* compact block reconstruction (no-missing case) */ +/* ================================================================ */ + +static void test_compact_block_reconstruct_no_missing(void) +{ + /* Build a minimal compact block with 1 prefilled tx and 0 short IDs. + * Reconstruction should succeed immediately with no missing transactions. */ + dogecoin_compact_block *cb = dogecoin_compact_block_new(); + memset(&cb->header, 0, sizeof(cb->header)); + cb->header.version = 1; + cb->nonce = 0; + cb->sipkey_k0 = 0; + cb->sipkey_k1 = 0; + cb->short_ids_count = 0; + cb->short_ids = NULL; + cb->prefilled_count = 1; + cb->prefilled_txs = dogecoin_calloc(1, sizeof(dogecoin_prefilled_tx)); + cb->prefilled_txs[0].index = 0; + cb->prefilled_txs[0].tx = dogecoin_tx_new(); + /* Leave tx fields at zero; reconstruction only places it, doesn't validate */ + + dogecoin_compact_block_state *state = dogecoin_compact_block_state_new(); + dogecoin_bool ok = dogecoin_compact_block_reconstruct(cb, state, NULL, 0); + u_assert_true(ok); + u_assert_uint32_eq(state->available_txs_count, 1); + u_assert_not_null(state->available_txs[0]); + u_assert_uint32_eq(state->missing_count, 0); + + /* The prefilled tx pointer is borrowed; do not free via state. + * compact_block_free will free the prefilled_tx. */ + state->available_txs[0] = NULL; + dogecoin_compact_block_state_free(state); + dogecoin_compact_block_free(cb); +} + +/* ================================================================ */ +/* Public test entry point */ +/* ================================================================ */ + +void test_compact_block(void) +{ + test_compact_block_new_free(); + test_getblocktxn_new_free(); + test_blocktxn_new_free(); + test_compact_block_state_new_free(); + test_sipkeys_deterministic(); + test_sipkeys_different_headers(); + test_short_id_deterministic(); + test_short_id_sensitivity(); + test_getblocktxn_ser_deser(); + test_getblocktxn_single_index(); + test_getblocktxn_zero_indices(); + test_compact_block_reconstruct_no_missing(); +} diff --git a/test/unittester.c b/test/unittester.c index 96dd35fae..96bcecbaf 100644 --- a/test/unittester.c +++ b/test/unittester.c @@ -46,6 +46,7 @@ extern void test_bip32(); extern void test_bip39(); extern void test_bip44(); extern void test_block_header(); +extern void test_compact_block(); extern void test_buffer(); extern void test_chacha20(); extern void test_cstr(); @@ -177,6 +178,7 @@ int main() u_run_test(test_bip39); u_run_test(test_bip44); u_run_test(test_block_header); + u_run_test(test_compact_block); u_run_test(test_buffer); u_run_test(test_chacha20); u_run_test(test_cstr); From ca95e6897ca95d6f01825d7acdb6ec35d158e4c9 Mon Sep 17 00:00:00 2001 From: bluezr Date: Sat, 1 Aug 2026 10:03:46 -0700 Subject: [PATCH 2/7] bip152: negotiate compact block relay with peers Announce compact block support once the handshake completes, and record what each peer announces back. fAnnounce is sent false: peers are asked not to push unsolicited cmpctblocks, which suits a client that drives its own block requests. Core does the same on its non-witness path, sending a single sendcmpct with version 1 after fSuccessfullyConnected. Version handling follows Core's SENDCMPCT handler, which enables compact blocks when the announced version is 1, or 2 only when the local node has NODE_WITNESS, and otherwise does nothing at all -- no disconnect, no misbehaviour score. Dogecoin has no witness serialization, so version 2 short ids (SipHash over the wtxid) cannot be computed here: a peer announcing it simply leaves cmpct_enabled false. That is a legitimate announcement on a witness chain rather than a protocol violation, so penalising it would be wrong. fAnnounce itself is a serialized bool and Core does not reject non-canonical encodings, so any non-zero byte is treated as true. The field only selects a relay preference, so rejecting a peer over it would risk interop damage while protecting nothing. Verified against a live 1.14.99 node: sendcmpct sent on our side, one received from the peer, and no version rejection logged -- the peer announced version 1, as expected on a pre-segwit chain. Reconstruction (cmpctblock, getblocktxn, blocktxn) follows separately. 79/79. --- include/dogecoin/compact_block.h | 17 +++++++++++++++ include/dogecoin/net.h | 8 +++++++ src/compact_block.c | 23 ++++++++++++++++++++ src/net.c | 37 ++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/include/dogecoin/compact_block.h b/include/dogecoin/compact_block.h index 0a0a2e735..76c42ca57 100644 --- a/include/dogecoin/compact_block.h +++ b/include/dogecoin/compact_block.h @@ -310,6 +310,23 @@ LIBDOGECOIN_API dogecoin_bool dogecoin_blocktxn_deserialize( * @param version Compact block version (1 for BIP152 v1). * @return P2P message cstring, or NULL on failure. Caller frees. */ +/** + * @brief Parse a sendcmpct payload: fAnnounce(1) | nCmpctVersion(8 LE). + * + * Both outputs are only written on success. Dogecoin is pre-SegWit, so the + * caller is expected to ignore any version other than CMPCTBLOCK_VERSION: + * version 2 short ids are computed over the wtxid, which does not exist here. + * + * @param high_bandwidth_out Receives the peer's fAnnounce preference. + * @param version_out Receives the announced compact block version. + * @param buf Message payload. + * @return true if the payload was well formed. + */ +LIBDOGECOIN_API dogecoin_bool dogecoin_p2p_msg_sendcmpct_deser( + dogecoin_bool *high_bandwidth_out, + uint64_t *version_out, + struct const_buffer *buf); + LIBDOGECOIN_API cstring *dogecoin_p2p_msg_sendcmpct( const unsigned char netmagic[4], dogecoin_bool high_bandwidth, diff --git a/include/dogecoin/net.h b/include/dogecoin/net.h index 7592500ef..7785b6b57 100644 --- a/include/dogecoin/net.h +++ b/include/dogecoin/net.h @@ -101,6 +101,14 @@ typedef struct dogecoin_node_ { unsigned int bestknownheight; + /* BIP152 compact block relay, negotiated per peer via sendcmpct. + * cmpct_version is only ever set to a version this build can honour -- + * Dogecoin is pre-SegWit, so that is version 1 (txid short ids) and a peer + * announcing version 2 leaves cmpct_enabled false. */ + dogecoin_bool cmpct_enabled; /* peer announced a version we support */ + dogecoin_bool cmpct_high_bandwidth; /* peer's fAnnounce: wants unsolicited cmpctblocks */ + uint64_t cmpct_version; /* negotiated version, 0 when unsupported */ + uint32_t hints; /* can be use for user defined state */ } dogecoin_node; diff --git a/src/compact_block.c b/src/compact_block.c index 9209e261e..5404c3c5d 100644 --- a/src/compact_block.c +++ b/src/compact_block.c @@ -451,6 +451,29 @@ dogecoin_bool dogecoin_blocktxn_deserialize( #ifdef WITH_NET +dogecoin_bool dogecoin_p2p_msg_sendcmpct_deser( + dogecoin_bool *high_bandwidth_out, + uint64_t *version_out, + struct const_buffer *buf) +{ + if (!buf) return false; + + uint8_t announce; + if (!deser_bytes(&announce, buf, 1)) return false; + /* fAnnounce is a serialized bool. Core deserializes it straight into a C++ + bool and does not reject non-canonical encodings, so treat any non-zero + byte as true rather than rejecting the message: the field only selects a + relay preference, so being stricter than the reference implementation + would risk penalising peers without protecting anything. */ + + uint64_t version; + if (!deser_u64(&version, buf)) return false; + + if (high_bandwidth_out) *high_bandwidth_out = (announce == 1); + if (version_out) *version_out = version; + return true; +} + cstring *dogecoin_p2p_msg_sendcmpct( const unsigned char netmagic[4], dogecoin_bool high_bandwidth, diff --git a/src/net.c b/src/net.c index 68f7efa97..528cbba64 100644 --- a/src/net.c +++ b/src/net.c @@ -65,6 +65,7 @@ #include #include #include +#include #include #include #include @@ -775,8 +776,44 @@ int dogecoin_node_parse_message(dogecoin_node* node, dogecoin_p2p_msg_hdr* hdr, } else if (strcmp(hdr->command, DOGECOIN_MSG_VERACK) == 0) { /* complete handshake if verack has been received */ node->version_handshake = true; + + /* BIP152: announce compact block support once the handshake is up. + fAnnounce is false -- we ask peers not to push unsolicited + cmpctblocks and instead fetch them ourselves, which suits a client + that drives its own block requests. Version 1 is the only version + that can be honoured here: version 2 computes short ids over the + wtxid, and Dogecoin has no witness serialization. */ + cstring* sendcmpct = dogecoin_p2p_msg_sendcmpct( + node->nodegroup->chainparams->netmagic, false, CMPCTBLOCK_VERSION); + if (sendcmpct) { + dogecoin_node_send(node, sendcmpct); + cstr_free(sendcmpct, true); + } + if (node->nodegroup->handshake_done_cb) node->nodegroup->handshake_done_cb(node); + } else if (strcmp(hdr->command, DOGECOIN_MSG_SENDCMPCT) == 0) { + dogecoin_bool hb = false; + uint64_t cmpct_version = 0; + if (!dogecoin_p2p_msg_sendcmpct_deser(&hb, &cmpct_version, buf)) { + return dogecoin_node_misbehave(node); + } + /* Record only what we can act on. A peer is free to announce version + 2; we simply do not enable compact blocks for it rather than + treating it as misbehaviour, since the announcement is legitimate + on a witness chain. */ + node->cmpct_high_bandwidth = hb; + if (cmpct_version == CMPCTBLOCK_VERSION) { + node->cmpct_enabled = true; + node->cmpct_version = cmpct_version; + } else { + node->cmpct_enabled = false; + node->cmpct_version = 0; + node->nodegroup->log_write_cb( + "node %d announced compact block version %llu; only version %d " + "is supported on a pre-segwit chain\n", + node->nodeid, (unsigned long long)cmpct_version, CMPCTBLOCK_VERSION); + } } else if (strcmp(hdr->command, DOGECOIN_MSG_PING) == 0) { uint64_t nonce = 0; if (!deser_u64(&nonce, buf)) { From 4d5743f16aa88756ec89dd8089a7b0d646e0d91b Mon Sep 17 00:00:00 2001 From: bluezr Date: Sat, 1 Aug 2026 10:18:08 -0700 Subject: [PATCH 3/7] bip152: harden compact block reconstruction before it is reachable Audit of dogecoin_compact_block_reconstruct() ahead of dispatching these messages. Nothing reaches this code today, so none of the below is currently exploitable -- which is exactly why it is worth fixing now, before the P2P wiring turns every one of them into a live path fed by an untrusted peer. - available_txs leaked on two error returns (the shortid_to_txpos and missing_indices allocation failures), and the invalid-prefilled-index path freed available_txs while leaving available_txs_count non-zero. A caller that then reached fill_missing() would index a NULL array through a still-positive count. All failures now funnel through one label that empties the state consistently. - Calling reconstruct twice on the same peer state leaked the previous available_txs and missing_indices. A peer can send back-to-back cmpctblocks, so reset at entry. - short_ids_count + prefilled_count could wrap: both are attacker supplied, and the sum is used as an allocation size and as the bound for every index check below it. Reject the overflow. - Duplicate prefilled indices silently overwrote a slot, desynchronising the short-id mapping so a slot existed that no short id could fill; the block could then never complete. Rejected. fill_missing() additionally refuses a state with no available_txs or missing_indices rather than dereferencing them on the strength of the counts alone. 79/79. --- src/compact_block.c | 54 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/src/compact_block.c b/src/compact_block.c index 5404c3c5d..4b0ec9168 100644 --- a/src/compact_block.c +++ b/src/compact_block.c @@ -560,7 +560,24 @@ dogecoin_bool dogecoin_compact_block_reconstruct( { if (!cmpctblk || !state) return false; - /* Total transaction count = short_ids + prefilled */ + /* Reset any layout left by a previous compact block on this peer's state, + otherwise a second cmpctblock leaks the earlier arrays. */ + if (state->available_txs) { + dogecoin_free(state->available_txs); + state->available_txs = NULL; + } + state->available_txs_count = 0; + if (state->missing_indices) { + dogecoin_free(state->missing_indices); + state->missing_indices = NULL; + } + state->missing_count = 0; + + /* Total transaction count = short_ids + prefilled. + Both are attacker-supplied counts, already bounded against the message + length at deserialization; guard the sum against overflow before it is + used as an allocation size. */ + if (cmpctblk->short_ids_count > UINT32_MAX - cmpctblk->prefilled_count) return false; uint32_t total_txs = cmpctblk->short_ids_count + cmpctblk->prefilled_count; if (total_txs == 0) return false; @@ -569,24 +586,27 @@ dogecoin_bool dogecoin_compact_block_reconstruct( if (!state->available_txs) return false; state->available_txs_count = total_txs; + uint32_t *shortid_to_txpos = NULL; + /* Place prefilled transactions at their correct indices */ uint32_t i; for (i = 0; i < cmpctblk->prefilled_count; i++) { uint32_t idx = cmpctblk->prefilled_txs[i].index; - if (idx >= total_txs) { - /* Invalid index */ - dogecoin_free(state->available_txs); - state->available_txs = NULL; - return false; - } + if (idx >= total_txs) goto fail; + /* Two prefilled entries claiming the same slot would silently overwrite + and desynchronise the short-id mapping built below, leaving a slot + that no short id can ever fill. Reject rather than mis-assemble. */ + if (state->available_txs[idx] != NULL) goto fail; state->available_txs[idx] = cmpctblk->prefilled_txs[i].tx; } /* Build a mapping from short_ids positions to available_txs positions. * short_ids[j] corresponds to the j-th non-prefilled slot. */ uint32_t short_idx = 0; - uint32_t *shortid_to_txpos = dogecoin_calloc(cmpctblk->short_ids_count, sizeof(uint32_t)); - if (!shortid_to_txpos && cmpctblk->short_ids_count > 0) return false; + if (cmpctblk->short_ids_count > 0) { + shortid_to_txpos = dogecoin_calloc(cmpctblk->short_ids_count, sizeof(uint32_t)); + if (!shortid_to_txpos) goto fail; + } for (i = 0; i < total_txs && short_idx < cmpctblk->short_ids_count; i++) { if (state->available_txs[i] == NULL) { @@ -630,10 +650,7 @@ dogecoin_bool dogecoin_compact_block_reconstruct( /* Build the missing indices list */ if (missing_count > 0) { state->missing_indices = dogecoin_calloc(missing_count, sizeof(uint32_t)); - if (!state->missing_indices) { - dogecoin_free(shortid_to_txpos); - return false; - } + if (!state->missing_indices) goto fail; state->missing_count = missing_count; uint32_t mi = 0; @@ -650,6 +667,16 @@ dogecoin_bool dogecoin_compact_block_reconstruct( dogecoin_free(shortid_to_txpos); return (missing_count == 0); + +fail: + /* Leave the state empty rather than half-built: available_txs_count must + never outlive the array it describes, or fill_missing() below indexes a + NULL pointer through a still-positive count. */ + dogecoin_free(shortid_to_txpos); + dogecoin_free(state->available_txs); + state->available_txs = NULL; + state->available_txs_count = 0; + return false; } dogecoin_bool dogecoin_compact_block_fill_missing( @@ -657,6 +684,7 @@ dogecoin_bool dogecoin_compact_block_fill_missing( const dogecoin_blocktxn *resp) { if (!state || !resp) return false; + if (!state->available_txs || !state->missing_indices) return false; if (resp->txs_count != state->missing_count) { /* Mismatch between requested and received transaction count */ From 4e2635f07d35a6a66fe9d66e6fd8ebe9344e5c78 Mon Sep 17 00:00:00 2001 From: bluezr Date: Sun, 2 Aug 2026 14:24:40 -0700 Subject: [PATCH 4/7] cmake: build compact_block.c and its tests, matching Makefile.am The Windows CI legs failed to link with: unittester.obj : error LNK2019: unresolved external symbol test_compact_block referenced in function main test/compact_block_tests.c was listed in Makefile.am but never added to the tests target in CMakeLists.txt, so under CMake unittester.c called a function that was never compiled. The Linux legs build with autotools and passed; only the three x86_64-win-native legs use CMake, which is why this looked platform-specific when it is really build-system-specific. Also moves src/compact_block.c out of IF(WITH_NET) to match where Makefile.am already has it. It includes only compact_block.h, mem, sha2, utils and portable_endian, and references no node, nodegroup or event_base symbol, so the guard was not buying anything -- and with the test now registered unconditionally, a -DWITH_NET=OFF build would have reproduced the same link error. CMake WITH_NET=ON: 79/79 (was 78, test_compact_block now runs) CMake WITH_NET=OFF: 73/73, previously would not have linked --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b0869042d..2a8f3fbcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -382,6 +382,7 @@ TARGET_SOURCES(${LIBDOGECOIN_NAME} PRIVATE src/buffer.c src/chacha20.c src/context.c + src/compact_block.c src/cstr.c src/ctaes.c src/ecc.c @@ -553,6 +554,7 @@ IF(USE_TESTS) test/block_tests.c test/buffer_tests.c test/chacha20_tests.c + test/compact_block_tests.c test/context_tests.c test/cstr_tests.c test/ecc_tests.c @@ -685,7 +687,6 @@ IF(WITH_NET) ) TARGET_SOURCES(${LIBDOGECOIN_NAME} ${visibility} src/bip37.c - src/compact_block.c src/headersdb_file.c src/net.c src/protocol.c From 111d65dee6342d8b643d9b3bebca058be98d930f Mon Sep 17 00:00:00 2001 From: xanimo Date: Mon, 3 Aug 2026 11:17:13 -0700 Subject: [PATCH 5/7] bip152: carry the cmpctblock header as Core serializes it, AuxPoW and all Core puts a CBlockHeader in the cmpctblock, not a CPureBlockHeader: blockencodings.h:146 CBlockHeader header; blockencodings.h:161 READWRITE(header); and CBlockHeader::SerializationOp (primitives/block.h) reads the pure header and then, conditionally, the AuxPoW: READWRITE(*(CPureBlockHeader*)this); if (this->IsAuxpow()) { ...; READWRITE(*auxpow); } with IsAuxpow() being exactly nVersion & VERSION_AUXPOW (pureheader.h:129). So the header field is 80 bytes for a non-merge-mined block and 80 bytes plus a full CAuxPow for everything else -- which, since height 371337, is effectively every block. It has to be: PartiallyDownloadedBlock::FillBlock runs CheckBlock, and the proof of work of a merge-mined block lives in the AuxPoW. A bare 80-byte header would be unverifiable. Match that dispatch: hand the whole buffer to dogecoin_block_header_deserialize and let it branch on 0x100 the way SerializationOp does. A peer that sets the bit without supplying AuxPoW fails in the AuxPoW sub-parsers, which is what Core does -- the stream read throws and the message is rejected. The larger divergence is in the keys. Core derives them over the header as it streams it: FillShortTxIDSelector(): stream << header << nonce; through the AuxPoW-bearing serializer. libdogecoin derived them from dogecoin_block_header_serialize, which emits only the six base fields. For any merge-mined block the preimage differs, so k0/k1 differ, so every short ID differs and reconstruction fails against every Core peer -- silently, at the reconstruct step, with nothing pointing back here. The parsed header cannot repair this. dogecoin_block_header_deserialize parses AuxPoW into a local dogecoin_auxpow_block and frees it at cleanup, and dogecoin_block_header_copy carries only auxpow->check, ->ctx and ->is: flags, not payload. By the time the keys are derived the bytes are gone. So retain them. dogecoin_compact_block gains header_raw/header_raw_len, holding the header exactly as it arrived; the deserializer measures the span by how far dogecoin_block_header_deserialize advanced the buffer. Keys come from dogecoin_compact_block_derive_sipkeys_raw over that span, which is Core's preimage by other means. The old dogecoin_compact_block_derive_sipkeys stays as a wrapper over the 80 base bytes, documented as correct only when !(version & 0x100). Serialization emits the retained span. Without one it emits the 80 base bytes if there is no AuxPoW to lose, and otherwise fails: there is no AuxPoW serializer in this tree, and putting 80 bytes on the wire for a merge-mined header produces a message Core rejects. Failing is better than failing silently, so dogecoin_compact_block_serialize now returns dogecoin_bool and dogecoin_p2p_msg_cmpctblock propagates it as NULL. This is an API break, but BIP152 has not shipped in a release. Tests. The height-371338 mainnet vector moves out of block_tests.c into test/data/auxpow_block_371338.h so both suites can use it; a real merge-mined header is the only honest fixture for this. The new round-trip builds a cmpctblock from that header and asserts the nonce is found behind the AuxPoW, that header_raw matches the wire bytes, that the keys equal an independently computed SHA256(span || nonce), that the 80-byte derivation gives different keys, and that re-serialization is byte-identical. A 0x100-without-AuxPoW message must be rejected. A third test covers the serialize refusal. Bounding the header to 80 bytes fails both new tests, the round-trip at the nonce: expect 81985529216486895, receive 4294967297 -- four bytes of the parent coinbase. check_auxpow running inside the deserializer, so that parsing a cmpctblock does scrypt work before any peer-level gating, is a separate problem and is left alone here. Core defers it to CheckBlock. WITH_NET=OFF: 72/72, valgrind --leak-check=full clean, 0 errors from 0 contexts. WITH_NET=ON: 74/74. --- Makefile.am | 1 + include/dogecoin/compact_block.h | 83 ++++++++++++++-- src/compact_block.c | 144 ++++++++++++++++++++++----- test/block_tests.c | 81 +--------------- test/compact_block_tests.c | 161 +++++++++++++++++++++++++++++++ test/data/auxpow_block_371338.h | 119 +++++++++++++++++++++++ 6 files changed, 483 insertions(+), 106 deletions(-) create mode 100644 test/data/auxpow_block_371338.h diff --git a/Makefile.am b/Makefile.am index d5f22d856..af3440d77 100644 --- a/Makefile.am +++ b/Makefile.am @@ -371,6 +371,7 @@ tests_SOURCES += \ test/raccoong_hash_vec_tests.c \ test/raccoong_buff_mu_tests.c \ test/raccoong_sign_tests.c \ + test/data/auxpow_block_371338.h \ test/data/raccoong_polyr_vectors.h \ test/data/raccoong_ntt_vectors.h \ test/data/raccoong_gaussian_vectors.h \ diff --git a/include/dogecoin/compact_block.h b/include/dogecoin/compact_block.h index 76c42ca57..b65e48d07 100644 --- a/include/dogecoin/compact_block.h +++ b/include/dogecoin/compact_block.h @@ -64,6 +64,15 @@ LIBDOGECOIN_BEGIN_DECL /** BIP152 compact block version (low-bandwidth relaying) */ #define CMPCTBLOCK_VERSION 1 +/* Base block header: version, prev_block, merkle_root, timestamp, bits, nonce. + This is the *minimum* size of the header field in a cmpctblock, not its size. + Dogecoin Core serializes CBlockHeader here (blockencodings.h), and + CBlockHeader::SerializationOp appends a full CAuxPow whenever + nVersion & VERSION_AUXPOW is set -- which is every merge-mined block, i.e. + effectively every block since height 371337. A cmpctblock header is therefore + 80 bytes for a non-merge-mined block and 80 + auxpow for everything else. */ +#define CMPCTBLOCK_HEADER_BASE_SIZE 80 + /** Short transaction ID length in bytes (6 bytes = 48 bits) */ #define SHORTTXID_LENGTH 6 @@ -93,7 +102,17 @@ typedef struct dogecoin_prefilled_tx_ { * the coinbase). */ typedef struct dogecoin_compact_block_ { - dogecoin_block_header header; /**< Block header (80 bytes, no auxpow serialized) */ + dogecoin_block_header header; /**< Parsed block header */ + /** Header exactly as it appears on the wire: the 80 base bytes, followed by + the AuxPoW blob when version bit 0x100 is set. Retained because the + parsed form cannot reproduce it -- dogecoin_block_header_deserialize + parses AuxPoW into a local dogecoin_auxpow_block and frees it, and + dogecoin_block_header_copy carries only the auxpow check/ctx/is flags, + not the payload. BIP152 derives the SipHash keys from these bytes, so + they must survive the parse or every short ID will differ from Core's. + NULL for a compact block assembled locally rather than parsed. */ + uint8_t *header_raw; + size_t header_raw_len; /**< Length of header_raw, 0 when unset */ uint64_t nonce; /**< Random nonce for SipHash key derivation */ uint64_t sipkey_k0; /**< SipHash key 0 (derived from header + nonce) */ uint64_t sipkey_k1; /**< SipHash key 1 (derived from header + nonce) */ @@ -203,12 +222,57 @@ LIBDOGECOIN_API void dogecoin_compact_block_state_free(dogecoin_compact_block_st /* ================================================================ */ /** - * @brief Derive the SipHash keys from a block header and nonce. + * @brief Attach the wire bytes of the header to a compact block. + * + * Copies len bytes; the compact block takes ownership of the copy and frees + * it. Any previously attached span is released. Required before serializing a + * compact block whose header carries AuxPoW, since the tree has no AuxPoW + * serializer to regenerate those bytes from the parsed form. + * + * @param cmpctblk The compact block. + * @param bytes Header bytes: 80 base bytes plus AuxPoW when present. + * @param len Length of bytes, at least CMPCTBLOCK_HEADER_BASE_SIZE. + * @return true on success, false on bad arguments or allocation failure. + */ +LIBDOGECOIN_API dogecoin_bool dogecoin_compact_block_set_header_raw( + dogecoin_compact_block *cmpctblk, + const void *bytes, + size_t len); + +/** + * @brief Derive the SipHash keys from the serialized header and nonce. + * + * Per BIP152: SHA256(block_header || nonce) produces k0 and k1 as the first + * two little-endian 64-bit integers of the hash. Core computes this over the + * header as it streams it -- CBlockHeaderAndShortTxIDs::FillShortTxIDSelector + * does `stream << header << nonce` through the AuxPoW-bearing serializer -- so + * for a merge-mined block the AuxPoW bytes are part of the preimage. + * + * @param header_ser Serialized header: 80 base bytes plus AuxPoW when present. + * @param header_ser_len Length of header_ser. + * @param nonce The compact block nonce. + * @param k0_out Output: SipHash key 0. + * @param k1_out Output: SipHash key 1. + */ +LIBDOGECOIN_API void dogecoin_compact_block_derive_sipkeys_raw( + const uint8_t *header_ser, + size_t header_ser_len, + uint64_t nonce, + uint64_t *k0_out, + uint64_t *k1_out); + +/** + * @brief Derive the SipHash keys from a parsed block header and nonce. + * + * Convenience wrapper that serializes the 80 base bytes and calls + * dogecoin_compact_block_derive_sipkeys_raw. * - * Per BIP152: SHA256(block_header || nonce) produces k0 and k1 as - * the first two little-endian 64-bit integers of the hash. + * Correct only when the header carries no AuxPoW, i.e. !(version & 0x100). + * For a merge-mined header the parsed form is missing the AuxPoW payload and + * the keys produced here will not match Core's. Use + * dogecoin_compact_block_derive_sipkeys_raw with the retained wire bytes. * - * @param header Serialized block header (80 bytes). + * @param header The parsed block header. * @param nonce The compact block nonce. * @param k0_out Output: SipHash key 0. * @param k1_out Output: SipHash key 1. @@ -241,10 +305,17 @@ LIBDOGECOIN_API void dogecoin_compact_block_compute_short_id( /** * @brief Serialize a compact block to a cstring. + * + * Emits the retained wire header when one is attached. Without one, falls back + * to the 80 base bytes, which is only valid for a header carrying no AuxPoW; + * for a merge-mined header with no retained span this fails rather than emit a + * truncated header that Core would reject. + * * @param s Output cstring. * @param cmpctblk The compact block to serialize. + * @return true on success, false if the header cannot be represented. */ -LIBDOGECOIN_API void dogecoin_compact_block_serialize(cstring *s, const dogecoin_compact_block *cmpctblk); +LIBDOGECOIN_API dogecoin_bool dogecoin_compact_block_serialize(cstring *s, const dogecoin_compact_block *cmpctblk); /** * @brief Deserialize a compact block from a buffer. diff --git a/src/compact_block.c b/src/compact_block.c index 4b0ec9168..faab07106 100644 --- a/src/compact_block.c +++ b/src/compact_block.c @@ -41,6 +41,7 @@ #include #include #include +#include /* ================================================================ */ /* Constructor / Destructor */ @@ -57,6 +58,12 @@ void dogecoin_compact_block_free(dogecoin_compact_block *cmpctblk) { if (!cmpctblk) return; + if (cmpctblk->header_raw) { + dogecoin_free(cmpctblk->header_raw); + cmpctblk->header_raw = NULL; + cmpctblk->header_raw_len = 0; + } + if (cmpctblk->short_ids) { dogecoin_free(cmpctblk->short_ids); cmpctblk->short_ids = NULL; @@ -156,30 +163,64 @@ void dogecoin_compact_block_state_free(dogecoin_compact_block_state *state) /* ================================================================ */ /** - * Per BIP152: The SipHash keys are derived from the SHA256 of - * the serialized block header (80 bytes) concatenated with the - * compact block nonce (8 bytes little-endian). + * @brief Attach the wire bytes of the header to a compact block. + * + * The parsed header cannot reproduce them: dogecoin_block_header_deserialize + * parses AuxPoW into a local dogecoin_auxpow_block and frees it at cleanup, + * and dogecoin_block_header_copy carries only the auxpow check/ctx/is flags, + * not the payload. Retaining the span is what lets the SipHash keys and the + * re-serialized message match what Core produced. + */ +dogecoin_bool dogecoin_compact_block_set_header_raw( + dogecoin_compact_block *cmpctblk, + const void *bytes, + size_t len) +{ + if (!cmpctblk || !bytes) return false; + if (len < CMPCTBLOCK_HEADER_BASE_SIZE) return false; + if (len > DOGECOIN_MAX_P2P_MSG_SIZE) return false; + + uint8_t *copy = dogecoin_malloc(len); + if (!copy) return false; + memcpy(copy, bytes, len); + + if (cmpctblk->header_raw) dogecoin_free(cmpctblk->header_raw); + cmpctblk->header_raw = copy; + cmpctblk->header_raw_len = len; + return true; +} + +/** + * Per BIP152: The SipHash keys are derived from the SHA256 of the serialized + * block header concatenated with the compact block nonce (8 bytes LE). + * + * "Serialized block header" is whatever the sender streamed, which for + * Dogecoin includes AuxPoW when the version bit is set -- Core's + * FillShortTxIDSelector does `stream << header << nonce` through + * CBlockHeader's serializer, not CPureBlockHeader's. * * k0 = first 8 bytes of SHA256 result (little-endian uint64) * k1 = next 8 bytes of SHA256 result (little-endian uint64) */ -void dogecoin_compact_block_derive_sipkeys( - const dogecoin_block_header *header, +void dogecoin_compact_block_derive_sipkeys_raw( + const uint8_t *header_ser, + size_t header_ser_len, uint64_t nonce, uint64_t *k0_out, uint64_t *k1_out) { - /* Serialize the 80-byte block header */ - cstring *hdr_ser = cstr_new_sz(88); - dogecoin_block_header_serialize(hdr_ser, header); + if (!header_ser || !k0_out || !k1_out) return; + + cstring *preimage = cstr_new_sz(header_ser_len + 8); + ser_bytes(preimage, header_ser, header_ser_len); /* Append the 8-byte nonce in little-endian */ - ser_u64(hdr_ser, nonce); + ser_u64(preimage, nonce); - /* SHA256(header || nonce) – single SHA256 per BIP152 */ + /* SHA256(header || nonce) - single SHA256 per BIP152 */ uint256_t hash; - sha256_raw((const uint8_t *)hdr_ser->str, hdr_ser->len, hash); - cstr_free(hdr_ser, true); + sha256_raw((const uint8_t *)preimage->str, preimage->len, hash); + cstr_free(preimage, true); /* Extract k0, k1 as little-endian uint64 from the hash */ const uint8_t *p = hash; @@ -194,6 +235,26 @@ void dogecoin_compact_block_derive_sipkeys( ((uint64_t)p[14] << 48) | ((uint64_t)p[15] << 56); } +/** + * Wrapper over the 80 base bytes. Only correct for a header with no AuxPoW; + * see the header file. + */ +void dogecoin_compact_block_derive_sipkeys( + const dogecoin_block_header *header, + uint64_t nonce, + uint64_t *k0_out, + uint64_t *k1_out) +{ + if (!header || !k0_out || !k1_out) return; + + cstring *hdr_ser = cstr_new_sz(CMPCTBLOCK_HEADER_BASE_SIZE); + dogecoin_block_header_serialize(hdr_ser, header); + dogecoin_compact_block_derive_sipkeys_raw((const uint8_t *)hdr_ser->str, + hdr_ser->len, nonce, + k0_out, k1_out); + cstr_free(hdr_ser, true); +} + /** * Per BIP152: ShortTxID = SipHash-2-4(k0, k1, txid) & 0xFFFFFFFFFFFF * The result is the lower 6 bytes in little-endian. @@ -219,12 +280,27 @@ void dogecoin_compact_block_compute_short_id( /* Serialization */ /* ================================================================ */ -void dogecoin_compact_block_serialize(cstring *s, const dogecoin_compact_block *cmpctblk) +dogecoin_bool dogecoin_compact_block_serialize(cstring *s, const dogecoin_compact_block *cmpctblk) { - if (!s || !cmpctblk) return; - - /* Block header (80 bytes) */ - dogecoin_block_header_serialize(s, &cmpctblk->header); + if (!s || !cmpctblk) return false; + + /* Block header: the bytes we received, if we have them. Core streams the + CBlockHeader it holds, AuxPoW included; replaying the retained span is + the same thing by other means, and it is the only way to reproduce a + merge-mined header -- the tree has no AuxPoW serializer, and the parsed + header keeps only the auxpow flags. */ + if (cmpctblk->header_raw && cmpctblk->header_raw_len >= CMPCTBLOCK_HEADER_BASE_SIZE) { + ser_bytes(s, cmpctblk->header_raw, cmpctblk->header_raw_len); + } else if ((cmpctblk->header.version & 0x100) == 0) { + /* No AuxPoW to lose: the 80 base bytes are the whole header. */ + dogecoin_block_header_serialize(s, &cmpctblk->header); + } else { + /* Merge-mined header with no retained span. Emitting 80 bytes here + would produce a message Core rejects, silently, so refuse instead. + Callers assembling such a block must supply the wire header via + dogecoin_compact_block_set_header_raw. */ + return false; + } /* Nonce (8 bytes LE) */ ser_u64(s, cmpctblk->nonce); @@ -249,6 +325,8 @@ void dogecoin_compact_block_serialize(cstring *s, const dogecoin_compact_block * dogecoin_tx_serialize(s, ptx->tx); last_index = ptx->index; } + + return true; } dogecoin_bool dogecoin_compact_block_deserialize( @@ -258,17 +336,36 @@ dogecoin_bool dogecoin_compact_block_deserialize( { if (!cmpctblk || !buf) return false; - /* Deserialize block header (80 bytes, no auxpow for compact blocks) */ + /* Deserialize block header. The field is a CBlockHeader in Core + (blockencodings.h), so it is 80 bytes plus a full CAuxPow whenever + version bit 0x100 is set -- which is every merge-mined block. Let + dogecoin_block_header_deserialize dispatch on that bit exactly as + CBlockHeader::SerializationOp does, and take the span it consumed. + + A peer that sets 0x100 without supplying AuxPoW fails here, in the + AuxPoW sub-parsers, which is what Core does too: the stream read throws + and the message is rejected. */ + const uint8_t *hdr_start = (const uint8_t *)buf->p; + if (buf->len < CMPCTBLOCK_HEADER_BASE_SIZE) return false; if (!dogecoin_block_header_deserialize(&cmpctblk->header, buf, params, NULL)) return false; + /* Retain the wire header. The SipHash keys are SHA256 over these bytes, + and the parsed header cannot regenerate the AuxPoW half of them. */ + size_t hdr_len = (size_t)((const uint8_t *)buf->p - hdr_start); + if (!dogecoin_compact_block_set_header_raw(cmpctblk, hdr_start, hdr_len)) + return false; + /* Nonce */ if (!deser_u64(&cmpctblk->nonce, buf)) return false; - /* Derive SipHash keys */ - dogecoin_compact_block_derive_sipkeys(&cmpctblk->header, cmpctblk->nonce, - &cmpctblk->sipkey_k0, &cmpctblk->sipkey_k1); + /* Derive SipHash keys over the header as it arrived, AuxPoW included */ + dogecoin_compact_block_derive_sipkeys_raw(cmpctblk->header_raw, + cmpctblk->header_raw_len, + cmpctblk->nonce, + &cmpctblk->sipkey_k0, + &cmpctblk->sipkey_k1); /* Short IDs */ if (!deser_varlen(&cmpctblk->short_ids_count, buf)) @@ -499,7 +596,10 @@ cstring *dogecoin_p2p_msg_cmpctblock( if (!cmpctblk) return NULL; cstring *payload = cstr_new_sz(512); - dogecoin_compact_block_serialize(payload, cmpctblk); + if (!dogecoin_compact_block_serialize(payload, cmpctblk)) { + cstr_free(payload, true); + return NULL; + } cstring *msg = dogecoin_p2p_message_new(netmagic, DOGECOIN_MSG_CMPCTBLOCK, payload->str, payload->len); diff --git a/test/block_tests.c b/test/block_tests.c index 3d27c9e76..edc28b89d 100644 --- a/test/block_tests.c +++ b/test/block_tests.c @@ -13,6 +13,8 @@ #include #include + +#include "data/auxpow_block_371338.h" #include #include @@ -235,84 +237,7 @@ void test_block_header() checked to link the aux block hash into the parent coinbase. A successful return is a true end-to-end validation of the auxpow deserializer. */ void test_auxpow_deserialize_real_vector() { - const char* block_hex = - "0201620053f0dc500d0fd8912622c5c2475f83529326c19dac4e955a1bffc5f9823932607df6" - "ee838b616413188439101f1c609b94e5143c431df75e0aab2fb2b647673661fb115490d4301b" - "0000000001000000010000000000000000000000000000000000000000000000000000000000" - "000000ffffffff380345bf09fabe6d6d5187f05b5b616c30c1945af345d1f95148963a12d0fb" - "215b8e7ad53a27161e3c08000000000000009bf8666459000000ffffffff01800c0c2a010000" - "001976a914aa3750aa18b8a0f3f0590731e1fab934856680cf88ac00000000b042af7b2a0bbb" - "7527fc48af101611276e60b91d2acc5875b8be25000000000003a979a636db2450363972d211" - "aee67b71387a3daaa3051be0fd260c5acd4739cd52a418d29d8a0e56c8714c95a0dc24e1c962" - "4480ec497fe2441941f3fee8f9481a3370c334178415c83d1d0c2deeec727c2330617a47691f" - "c5e79203669312d100000000036fa40307b3a439538195245b0de56a2c1db6ba3a64f8bdd207" - "1d00bc48c841b5e77b98e5c7d6f06f92dec5cf6d61277ecb9a0342406f49f34c51ee8ce4abd6" - "78038129485de14238bd1ca12cd2de12ff0e383aee542d90437cd664ce139446a00000000002" - "000000d2ec7dfeb7e8f43fe77aba3368df95ac2088034420402730ee0492a208421708f6d32f" - "6e7eb2941bcdcd47740f7c67a7b1930014b771a18809a86898a506250f60fb11548b54021be0" - "17686d0601000000010000000000000000000000000000000000000000000000000000000000" - "000000ffffffff0d038aaa050101062f503253482fffffffff010109d54eaf050000232102b7" - "3438165461b826b30a46078f211aa005d1e7e430b1e0ed461678a5fe516c73ac000000000100" - "000009bc1d305c59dd1809a6546010bbc43e8c25ad5d241cd83e582010f9930677ae8a270000" - "006a473044022100ece82d985cfedfb30b9227ae71bf154673beeba85a8667dcb492fedfc8b6" - "d87f021f2cb89b285b0f7964f613696c0b7b27c4f1476b954df17f63a2d7d23b559218012103" - "bb439b7328630b2985dd73c711d79dfa54e644fe33e44de49d5475e7f6fde985ffffffffcd1a" - "eddb2ae887734ec3a16aff82c198d83af739e1923cf93aa28d74519665a1010000006b483045" - "022100e4ac71b6650c586f04e8ca9d37bf7fbf2483107dae076bffb06d5c6cd471b90502201f" - "d946bb433637c1c05170f7f2af13d82b4d37595659b85eadec8db85f9b48fd0121024cc4de99" - "ad5ebf4c0ff81d66b5d1953ec7b28cb7e558ef758376e685391badecffffffff182fe5662129" - "bc59a78ef49bddeab1281d34cf9dbdac3cbfb9e0090051789bd03b0000006a4730440220527b" - "19bb4858ace5f9e3e12cc68c91b04cb2cc8af88e4a3efbbca40f107ad2310220574bace2eda1" - "1bce2e7dd593642c3bd537a0075448378cc185cd65953f985f9701210256bdef145ceaddaf7a" - "fc1ece0fecfd6548f3d34e54a3123d5f36c75b15fbc2feffffffff15c0dd130c3c37f6765465" - "c2659d4ef8cc7c8a382baad02fafbd3fd527f7bfb1010000006b483045022100bc05ef192be8" - "fb2f272efb7be8158815251f88fed7878e5df4431aa94585a2ef02203e7194d0cd4f8481e8a8" - "be7e24474aced48a712cfef64b0fc1f10ca7faea2f8f012103eae1c39387a87e782810342bcd" - "59b11b6ba18aee1fcb9997744b649b76707624ffffffff12d739cc5cb2cb80c4cd8b76d5a9e2" - "d8b29a2752be8f5d5fd2d8f6757c817fe4010000006b483045022100a1870aa5d56b0c79cdd7" - "b119f5377cb05d4c4bd1dc1cf553598c2364d0d99df8022067ea779f0f19bc992b4660820ae9" - "9c27b452049a70ac7efa7b73ef4d0e5ddb19012103f0f23aad840a2270d53501cc008a8953ff" - "d867dec5d2535ea6ba0c17f4963615ffffffffd7fe105770ec91f57f066b5ec91930a117b882" - "e47ceafc376dadd1fe2c285a10010000006a473044022072b5345c6b4bb5496862393d49618f" - "ef778ad879fc94b24bcac9b5595143557a02200e919b915e3cecde82ce6f95a7a52844ca44d5" - "cc553ed75e1a4de452218c778e012102fd3c13362dc56adfe990de025274593cbf3294d09a78" - "c002119120056087df90ffffffffba55152dd60fe6c3bdd078047e4812f642b5af890efd14fe" - "2f1a733999929934010000006a473044022000cb23f9ea744e293f87750cd154ab96a06a6178" - "ea4323e3d7f5a6458a25f41002200a73f4f26d48ce022c3f7eee520eb4ee57a88b8b8d984cf7" - "597ce032eac7df8b01210304a5713d3350d4181e4e2eaf923e25a6820d44a8b82e731fd37ff1" - "32c459bb2cffffffff50e7e2a34b00dc3cd664af02fcdba9ac22de118fa24bbff7ea6f49bb4c" - "93f374000000006b483045022100efbaebfe27f2fa925ab4e98b4040119ce4347a7cde53cbdc" - "42b4c1243211eb4c02201fac79a49a6201aa17051ca1ad6c12d0a52582cc1c19310e2eff204b" - "828a8077012103433e7a65c0b66947d98f92ae0d5e4ccf9fe0a1eba9934098614fc1bf2a4d2b" - "33ffffffff59a7866891da6616de39d60799d9a328de26b68eb122482bba0e1028fe18405500" - "0000006a47304402205209f3da5971ac77ce54229aeff8a08833f08cee4c074a156dbfd7041c" - "0a19fb02201242b373983a2d8a74c4ab52206cd4fb4a65ce0fe4e5d62f90333cd929a2ee6c01" - "210270b236c412b4fae735368f194dbb14bb22f6bb55f907699d42295e45a1dc364affffffff" - "02b47aba8dae1c00001976a91413b37bc2a0b57fd3740f3632c5d0038b49173eff88acb464ff" - "05000000001976a914ff4dfe9fc4fce9b1a118d4ca18d634452ccef14188ac00000000010000" - "000159a484082af8d2311cce82e8571ae1ef738f03aee982d4fee2b397e7f0e8045d01000000" - "6c493046022100c73ff48629dd1a56d167879a0da40439b5700dbe89635a7dc46a80ae76aa56" - "50022100db7dc935ff9b52a2a494181b83aa1b5dcf472e70b228e6672482279f8aa56f5a0121" - "033fcc1cb9c1b7b11758eb2cd3a25b4ff917a5e248f5d6fcc74160dd6a450acf8bffffffff02" - "0061e600030000001976a9140fa60d44a3a53066ada76ea81e8c2a313e08899b88ac18eeb300" - "64ae04001976a914a1ea13863020f36897b671ad328d98e9364f12b488ac0000000001000000" - "01ba8131daa48a8f43b1153f54fd627d0f7f98d020fc3af349741668db359efc83010000006b" - "4830450220523bd4c8478af0345b1f91ff66a04ae3487f708dfdcabc16cd984d1ce7022bbf02" - "2100b02950d0be848a5436fec0d03f52bdba8477a47a6f9085ce5b1aff0488e4ede5012102e2" - "89b43973439cba87a5466abaa425262244e242d1082b0ec58dbeba2b0d225bffffffff026180" - "288b050000001976a914b7ddc901f636827e5766d1920f3892d4d4bee50088ac78b8dccd9b02" - "00001976a91427fe37db47615924826ae39169005a14f3e9cafe88ac000000000100000001ee" - "1cf26c8d505d963a2fc659d6ba9a68f46544e01bae0af69a5a5015ff821440010000006c4930" - "46022100c4bbcd51537d033a2bdbb3ac0b999410397aca873b5ece0d8fdabb874a4056bf0221" - "00cac7617e110a20856f04ef1498195867d0772e3e148f6a2a03ed45fe2cfc36a70121033fcc" - "1cb9c1b7b11758eb2cd3a25b4ff917a5e248f5d6fcc74160dd6a450acf8bffffffff0200aea6" - "8f020000001976a9141be533a3aff7aec0bfca73c5b7407627d018265788acaa5f8470877900" - "001976a914a1ea13863020f36897b671ad328d98e9364f12b488ac000000000100000001369e" - "1a4c499dfe270f99426ee906e39a525c04ca23a266e7195ad0ed80888454000000006a473044" - "02201ba3e871eb8dcbf3edc26bab4a36eac3e676c573fe3d527bd75a862e51ed8a8a022023e9" - "7b7783d840eb17578ea98f51613a4e7aa0752dd98c3f67f8b93b3f9790a601210215323532e0" - "d509a3237519c489050351c7ef194d7ef0b0f74ce58097b6b335f4ffffffff0100005a620200" - "00001976a91481db1aa49ebc6a71cad96949eb28e22af85eb0bd88ac00000000"; + const char* block_hex = auxpow_block_371338_hex; size_t hexlen = strlen(block_hex); size_t blen = hexlen / 2; diff --git a/test/compact_block_tests.c b/test/compact_block_tests.c index 2ae1b1fc6..51dc8137e 100644 --- a/test/compact_block_tests.c +++ b/test/compact_block_tests.c @@ -33,6 +33,10 @@ #include #include #include +#include +#include + +#include "data/auxpow_block_371338.h" /* ================================================================ */ /* Lifecycle */ @@ -293,6 +297,160 @@ static void test_compact_block_reconstruct_no_missing(void) /* Public test entry point */ /* ================================================================ */ +/* Core puts a CBlockHeader in the cmpctblock (blockencodings.h:146), and + CBlockHeader::SerializationOp appends a full CAuxPow whenever the version + carries 0x100. A peer that sets the bit and then supplies no AuxPoW is + sending a message Core cannot read: the stream throws partway through the + parent coinbase and the message is rejected. Assert we reject it too, rather + than reinterpreting the nonce and vectors as a bare header's worth of + trailing data. */ +static void test_compact_block_auxpow_bit_without_body_is_rejected(void) +{ + cstring *msg = cstr_new_sz(128); + + ser_s32(msg, 0x00620102); + unsigned char prev[32], merkle[32]; + memset(prev, 0x11, sizeof(prev)); + memset(merkle, 0x22, sizeof(merkle)); + ser_bytes(msg, prev, 32); + ser_bytes(msg, merkle, 32); + ser_u32(msg, 0x5f5e1000); + ser_u32(msg, 0x1e0ffff0); + ser_u32(msg, 0xdeadbeef); + u_assert_uint32_eq((uint32_t)msg->len, CMPCTBLOCK_HEADER_BASE_SIZE); + + /* Everything after the base header is nonce + empty vectors, not AuxPoW */ + ser_u64(msg, 0x0123456789abcdefULL); + ser_varlen(msg, 0); + ser_varlen(msg, 0); + + dogecoin_compact_block *cb = dogecoin_compact_block_new(); + u_assert_not_null(cb); + + struct const_buffer buf = { msg->str, msg->len }; + dogecoin_bool ok = dogecoin_compact_block_deserialize(cb, &buf, + &dogecoin_chainparams_main); + u_assert_int_eq(ok, false); + + dogecoin_compact_block_free(cb); + cstr_free(msg, true); +} + +/* The header field of a real Dogecoin cmpctblock is 80 bytes plus AuxPoW, + because essentially every block since 371337 is merge-mined. Build one from + the height-371338 mainnet vector and check the three things that follow from + that: + + - the parse consumes the whole AuxPoW header and finds the nonce behind it; + - the retained span is exactly what arrived, so re-serializing reproduces + the message byte for byte; + - the SipHash keys are SHA256 over that span, AuxPoW included, matching + Core's FillShortTxIDSelector. Deriving them from the parsed header + instead -- 80 bytes, AuxPoW payload dropped by the parse -- gives + different keys and therefore short IDs no Core peer would agree with. */ +static void test_compact_block_real_auxpow_header_roundtrip(void) +{ + size_t hexlen = strlen(auxpow_block_371338_hex); + size_t blen = hexlen / 2; + uint8_t *raw = dogecoin_malloc(blen); + u_assert_not_null(raw); + utils_hex_to_bin(auxpow_block_371338_hex, raw, hexlen, &blen); + + /* Measure the header span the way the deserializer will: parse the block's + header out of the full serialized block and see how far it advanced. + Everything past that point is the block's transaction vector. */ + dogecoin_block_header *probe = dogecoin_block_header_new(); + struct const_buffer pb = { raw, blen }; + u_assert_int_eq(dogecoin_block_header_deserialize(probe, &pb, + &dogecoin_chainparams_main, NULL), 1); + size_t hdr_span = blen - pb.len; + u_assert_int_eq(hdr_span > CMPCTBLOCK_HEADER_BASE_SIZE, 1); + dogecoin_block_header_free(probe); + + /* cmpctblock =
|| nonce || 0 short ids || 0 prefilled */ + const uint64_t nonce = 0x0123456789abcdefULL; + cstring *msg = cstr_new_sz(hdr_span + 16); + ser_bytes(msg, raw, hdr_span); + ser_u64(msg, nonce); + ser_varlen(msg, 0); + ser_varlen(msg, 0); + + dogecoin_compact_block *cb = dogecoin_compact_block_new(); + u_assert_not_null(cb); + + struct const_buffer buf = { msg->str, msg->len }; + u_assert_int_eq(dogecoin_compact_block_deserialize(cb, &buf, + &dogecoin_chainparams_main), true); + + /* The header parsed is the height-371338 aux header */ + u_assert_uint32_eq((uint32_t)cb->header.version, 0x00620102); + u_assert_uint32_eq(cb->header.timestamp, 1410464609); + + /* The nonce was found behind the AuxPoW, not inside it */ + u_assert_uint64_eq(cb->nonce, nonce); + u_assert_uint32_eq(cb->short_ids_count, 0); + u_assert_uint32_eq(cb->prefilled_count, 0); + + /* The retained span is the wire header, AuxPoW and all */ + u_assert_int_eq(cb->header_raw_len == hdr_span, 1); + u_assert_int_eq(memcmp(cb->header_raw, raw, hdr_span), 0); + + /* Keys are SHA256(header_span || nonce_le), computed here independently */ + cstring *preimage = cstr_new_sz(hdr_span + 8); + ser_bytes(preimage, raw, hdr_span); + ser_u64(preimage, nonce); + uint256_t expect; + sha256_raw((const uint8_t *)preimage->str, preimage->len, expect); + cstr_free(preimage, true); + + uint64_t k0 = 0, k1 = 0; + memcpy(&k0, expect, 8); + memcpy(&k1, expect + 8, 8); + k0 = le64toh(k0); + k1 = le64toh(k1); + u_assert_int_eq(cb->sipkey_k0 == k0, 1); + u_assert_int_eq(cb->sipkey_k1 == k1, 1); + + /* The 80-byte derivation is a different preimage and must not collide */ + uint64_t bare_k0 = 0, bare_k1 = 0; + dogecoin_compact_block_derive_sipkeys(&cb->header, nonce, &bare_k0, &bare_k1); + u_assert_int_eq(bare_k0 == cb->sipkey_k0, 0); + + /* Re-serializing reproduces the message exactly */ + cstring *out = cstr_new_sz(msg->len); + u_assert_int_eq(dogecoin_compact_block_serialize(out, cb), true); + u_assert_int_eq(out->len == msg->len, 1); + u_assert_int_eq(memcmp(out->str, msg->str, msg->len), 0); + cstr_free(out, true); + + dogecoin_compact_block_free(cb); + cstr_free(msg, true); + dogecoin_free(raw); +} + +/* Without a retained span there is nothing to emit for a merge-mined header: + dogecoin_block_header_copy carries the auxpow flags but not the payload, and + there is no AuxPoW serializer in the tree. Serializing 80 bytes anyway would + put a header on the wire that Core rejects, so the call fails instead. */ +static void test_compact_block_serialize_refuses_headerless_auxpow(void) +{ + dogecoin_compact_block *cb = dogecoin_compact_block_new(); + u_assert_not_null(cb); + cb->header.version = 0x00620102; + cb->nonce = 7; + + cstring *out = cstr_new_sz(128); + u_assert_int_eq(dogecoin_compact_block_serialize(out, cb), false); + + /* The same block without the auxpow bit serializes from the parsed header */ + cb->header.version = 0x00000002; + u_assert_int_eq(dogecoin_compact_block_serialize(out, cb), true); + u_assert_int_eq(out->len >= CMPCTBLOCK_HEADER_BASE_SIZE, 1); + + cstr_free(out, true); + dogecoin_compact_block_free(cb); +} + void test_compact_block(void) { test_compact_block_new_free(); @@ -307,4 +465,7 @@ void test_compact_block(void) test_getblocktxn_single_index(); test_getblocktxn_zero_indices(); test_compact_block_reconstruct_no_missing(); + test_compact_block_auxpow_bit_without_body_is_rejected(); + test_compact_block_real_auxpow_header_roundtrip(); + test_compact_block_serialize_refuses_headerless_auxpow(); } diff --git a/test/data/auxpow_block_371338.h b/test/data/auxpow_block_371338.h new file mode 100644 index 000000000..09843d114 --- /dev/null +++ b/test/data/auxpow_block_371338.h @@ -0,0 +1,119 @@ +/* + + The MIT License (MIT) + + Copyright (c) 2026 The Dogecoin Foundation + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +/* Real mainnet auxpow block, height 371338, hash + 6fb5ae70a65902381bcaa63a38cadf36e8c0a9b4cbffa85bc545c1be08cd0721. + Full serialized block as returned by getblock 0: 80-byte aux header, + parent coinbase transaction, parent hash, parent coinbase merkle branch, + chain merkle branch, parent header, then the block's transactions. + + Shared by block_tests.c (auxpow deserialization) and compact_block_tests.c + (BIP152 header span), which needs a header Core would actually put in a + cmpctblock -- i.e. a merge-mined one. */ + +#ifndef __LIBDOGECOIN_TEST_AUXPOW_BLOCK_371338_H__ +#define __LIBDOGECOIN_TEST_AUXPOW_BLOCK_371338_H__ + +static const char auxpow_block_371338_hex[] = + "0201620053f0dc500d0fd8912622c5c2475f83529326c19dac4e955a1bffc5f9823932607df6" + "ee838b616413188439101f1c609b94e5143c431df75e0aab2fb2b647673661fb115490d4301b" + "0000000001000000010000000000000000000000000000000000000000000000000000000000" + "000000ffffffff380345bf09fabe6d6d5187f05b5b616c30c1945af345d1f95148963a12d0fb" + "215b8e7ad53a27161e3c08000000000000009bf8666459000000ffffffff01800c0c2a010000" + "001976a914aa3750aa18b8a0f3f0590731e1fab934856680cf88ac00000000b042af7b2a0bbb" + "7527fc48af101611276e60b91d2acc5875b8be25000000000003a979a636db2450363972d211" + "aee67b71387a3daaa3051be0fd260c5acd4739cd52a418d29d8a0e56c8714c95a0dc24e1c962" + "4480ec497fe2441941f3fee8f9481a3370c334178415c83d1d0c2deeec727c2330617a47691f" + "c5e79203669312d100000000036fa40307b3a439538195245b0de56a2c1db6ba3a64f8bdd207" + "1d00bc48c841b5e77b98e5c7d6f06f92dec5cf6d61277ecb9a0342406f49f34c51ee8ce4abd6" + "78038129485de14238bd1ca12cd2de12ff0e383aee542d90437cd664ce139446a00000000002" + "000000d2ec7dfeb7e8f43fe77aba3368df95ac2088034420402730ee0492a208421708f6d32f" + "6e7eb2941bcdcd47740f7c67a7b1930014b771a18809a86898a506250f60fb11548b54021be0" + "17686d0601000000010000000000000000000000000000000000000000000000000000000000" + "000000ffffffff0d038aaa050101062f503253482fffffffff010109d54eaf050000232102b7" + "3438165461b826b30a46078f211aa005d1e7e430b1e0ed461678a5fe516c73ac000000000100" + "000009bc1d305c59dd1809a6546010bbc43e8c25ad5d241cd83e582010f9930677ae8a270000" + "006a473044022100ece82d985cfedfb30b9227ae71bf154673beeba85a8667dcb492fedfc8b6" + "d87f021f2cb89b285b0f7964f613696c0b7b27c4f1476b954df17f63a2d7d23b559218012103" + "bb439b7328630b2985dd73c711d79dfa54e644fe33e44de49d5475e7f6fde985ffffffffcd1a" + "eddb2ae887734ec3a16aff82c198d83af739e1923cf93aa28d74519665a1010000006b483045" + "022100e4ac71b6650c586f04e8ca9d37bf7fbf2483107dae076bffb06d5c6cd471b90502201f" + "d946bb433637c1c05170f7f2af13d82b4d37595659b85eadec8db85f9b48fd0121024cc4de99" + "ad5ebf4c0ff81d66b5d1953ec7b28cb7e558ef758376e685391badecffffffff182fe5662129" + "bc59a78ef49bddeab1281d34cf9dbdac3cbfb9e0090051789bd03b0000006a4730440220527b" + "19bb4858ace5f9e3e12cc68c91b04cb2cc8af88e4a3efbbca40f107ad2310220574bace2eda1" + "1bce2e7dd593642c3bd537a0075448378cc185cd65953f985f9701210256bdef145ceaddaf7a" + "fc1ece0fecfd6548f3d34e54a3123d5f36c75b15fbc2feffffffff15c0dd130c3c37f6765465" + "c2659d4ef8cc7c8a382baad02fafbd3fd527f7bfb1010000006b483045022100bc05ef192be8" + "fb2f272efb7be8158815251f88fed7878e5df4431aa94585a2ef02203e7194d0cd4f8481e8a8" + "be7e24474aced48a712cfef64b0fc1f10ca7faea2f8f012103eae1c39387a87e782810342bcd" + "59b11b6ba18aee1fcb9997744b649b76707624ffffffff12d739cc5cb2cb80c4cd8b76d5a9e2" + "d8b29a2752be8f5d5fd2d8f6757c817fe4010000006b483045022100a1870aa5d56b0c79cdd7" + "b119f5377cb05d4c4bd1dc1cf553598c2364d0d99df8022067ea779f0f19bc992b4660820ae9" + "9c27b452049a70ac7efa7b73ef4d0e5ddb19012103f0f23aad840a2270d53501cc008a8953ff" + "d867dec5d2535ea6ba0c17f4963615ffffffffd7fe105770ec91f57f066b5ec91930a117b882" + "e47ceafc376dadd1fe2c285a10010000006a473044022072b5345c6b4bb5496862393d49618f" + "ef778ad879fc94b24bcac9b5595143557a02200e919b915e3cecde82ce6f95a7a52844ca44d5" + "cc553ed75e1a4de452218c778e012102fd3c13362dc56adfe990de025274593cbf3294d09a78" + "c002119120056087df90ffffffffba55152dd60fe6c3bdd078047e4812f642b5af890efd14fe" + "2f1a733999929934010000006a473044022000cb23f9ea744e293f87750cd154ab96a06a6178" + "ea4323e3d7f5a6458a25f41002200a73f4f26d48ce022c3f7eee520eb4ee57a88b8b8d984cf7" + "597ce032eac7df8b01210304a5713d3350d4181e4e2eaf923e25a6820d44a8b82e731fd37ff1" + "32c459bb2cffffffff50e7e2a34b00dc3cd664af02fcdba9ac22de118fa24bbff7ea6f49bb4c" + "93f374000000006b483045022100efbaebfe27f2fa925ab4e98b4040119ce4347a7cde53cbdc" + "42b4c1243211eb4c02201fac79a49a6201aa17051ca1ad6c12d0a52582cc1c19310e2eff204b" + "828a8077012103433e7a65c0b66947d98f92ae0d5e4ccf9fe0a1eba9934098614fc1bf2a4d2b" + "33ffffffff59a7866891da6616de39d60799d9a328de26b68eb122482bba0e1028fe18405500" + "0000006a47304402205209f3da5971ac77ce54229aeff8a08833f08cee4c074a156dbfd7041c" + "0a19fb02201242b373983a2d8a74c4ab52206cd4fb4a65ce0fe4e5d62f90333cd929a2ee6c01" + "210270b236c412b4fae735368f194dbb14bb22f6bb55f907699d42295e45a1dc364affffffff" + "02b47aba8dae1c00001976a91413b37bc2a0b57fd3740f3632c5d0038b49173eff88acb464ff" + "05000000001976a914ff4dfe9fc4fce9b1a118d4ca18d634452ccef14188ac00000000010000" + "000159a484082af8d2311cce82e8571ae1ef738f03aee982d4fee2b397e7f0e8045d01000000" + "6c493046022100c73ff48629dd1a56d167879a0da40439b5700dbe89635a7dc46a80ae76aa56" + "50022100db7dc935ff9b52a2a494181b83aa1b5dcf472e70b228e6672482279f8aa56f5a0121" + "033fcc1cb9c1b7b11758eb2cd3a25b4ff917a5e248f5d6fcc74160dd6a450acf8bffffffff02" + "0061e600030000001976a9140fa60d44a3a53066ada76ea81e8c2a313e08899b88ac18eeb300" + "64ae04001976a914a1ea13863020f36897b671ad328d98e9364f12b488ac0000000001000000" + "01ba8131daa48a8f43b1153f54fd627d0f7f98d020fc3af349741668db359efc83010000006b" + "4830450220523bd4c8478af0345b1f91ff66a04ae3487f708dfdcabc16cd984d1ce7022bbf02" + "2100b02950d0be848a5436fec0d03f52bdba8477a47a6f9085ce5b1aff0488e4ede5012102e2" + "89b43973439cba87a5466abaa425262244e242d1082b0ec58dbeba2b0d225bffffffff026180" + "288b050000001976a914b7ddc901f636827e5766d1920f3892d4d4bee50088ac78b8dccd9b02" + "00001976a91427fe37db47615924826ae39169005a14f3e9cafe88ac000000000100000001ee" + "1cf26c8d505d963a2fc659d6ba9a68f46544e01bae0af69a5a5015ff821440010000006c4930" + "46022100c4bbcd51537d033a2bdbb3ac0b999410397aca873b5ece0d8fdabb874a4056bf0221" + "00cac7617e110a20856f04ef1498195867d0772e3e148f6a2a03ed45fe2cfc36a70121033fcc" + "1cb9c1b7b11758eb2cd3a25b4ff917a5e248f5d6fcc74160dd6a450acf8bffffffff0200aea6" + "8f020000001976a9141be533a3aff7aec0bfca73c5b7407627d018265788acaa5f8470877900" + "001976a914a1ea13863020f36897b671ad328d98e9364f12b488ac000000000100000001369e" + "1a4c499dfe270f99426ee906e39a525c04ca23a266e7195ad0ed80888454000000006a473044" + "02201ba3e871eb8dcbf3edc26bab4a36eac3e676c573fe3d527bd75a862e51ed8a8a022023e9" + "7b7783d840eb17578ea98f51613a4e7aa0752dd98c3f67f8b93b3f9790a601210215323532e0" + "d509a3237519c489050351c7ef194d7ef0b0f74ce58097b6b335f4ffffffff0100005a620200" + "00001976a91481db1aa49ebc6a71cad96949eb28e22af85eb0bd88ac00000000"; + +#endif /* __LIBDOGECOIN_TEST_AUXPOW_BLOCK_371338_H__ */ From 5bf4653e73234f46d8890b13dc567ecdd0b82b87 Mon Sep 17 00:00:00 2001 From: bluezr Date: Sun, 2 Aug 2026 22:16:10 -0700 Subject: [PATCH 6/7] fuzz: BIP152 compact block deserializer harness Adds a libFuzzer harness for the four BIP152 deserializers, on the infrastructure from #351. All parse peer-supplied bytes before validation: cmpctblock header | nonce | vec | vec getblocktxn block_hash | vec blocktxn block_hash | vec sendcmpct announce (1) | version (8) cmpctblock and getblocktxn are the interesting pair: both carry indices that are differentially encoded, so the parser accumulates a running total from attacker-controlled deltas. The harness exposed a divergence from Core that is tracked separately: dogecoin_compact_block_derive_sipkeys serializes the header with dogecoin_block_header_serialize, which emits only the six 80-byte fields, while Core's FillShortTxIDSelector streams the auxpow-bearing serializer. For any merge-mined block the k0/k1 differ and every short ID mismatches, so reconstruction fails silently. The header struct cannot currently express the difference: the auxpow payload is parsed into a local dogecoin_auxpow_block and freed, so the bytes needed to reproduce Core's hash are discarded before the compact block ever sees them. Result: 2,409,938 executions, 1119 new coverage units, no crashes. Two notes on how the numbers were obtained. The first version of this harness deserialized into stack structs and died two executions in: dogecoin_compact_block_free and friends release the container as well as its members, so they need their paired _new. ASAN caught it immediately, which is the harness working as intended rather than a defect in the API. Long runs are recorded with detect_leaks=0. libFuzzer runs LeakSanitizer periodically during a run, and its stop-the-world scan is unreliable on this host -- identical input and identical binary segfault roughly three times in eight, always after the target has finished and written its output. Bounded runs with leak detection enabled are clean and report no leaks; the instability is the environment, not the code. --- Makefile.am | 9 +++- fuzz/fuzz_compact_block.c | 109 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 fuzz/fuzz_compact_block.c diff --git a/Makefile.am b/Makefile.am index af3440d77..7d6ee93e8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -506,6 +506,13 @@ fuzz_fuzz_tx_LDADD = libdogecoin.la $(ASM_LIB_FILES) fuzz_fuzz_tx_CFLAGS = $(AM_CFLAGS) -fsanitize=fuzzer,address,undefined fuzz_fuzz_tx_LDFLAGS = -fsanitize=fuzzer,address,undefined -static +noinst_PROGRAMS += fuzz/fuzz_compact_block +fuzz_fuzz_compact_block_SOURCES = fuzz/fuzz_compact_block.c +fuzz_fuzz_compact_block_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/include +fuzz_fuzz_compact_block_LDADD = libdogecoin.la $(ASM_LIB_FILES) +fuzz_fuzz_compact_block_CFLAGS = $(AM_CFLAGS) -fsanitize=fuzzer,address,undefined +fuzz_fuzz_compact_block_LDFLAGS = -fsanitize=fuzzer,address,undefined -static + noinst_PROGRAMS += fuzz/fuzz_block fuzz_fuzz_block_SOURCES = fuzz/fuzz_block.c fuzz_fuzz_block_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/include @@ -562,7 +569,7 @@ fuzz-corpus: fuzz/seed_logdb_corpus ./fuzz/seed_logdb_corpus $(CORPUS) -FUZZ_TARGETS = fuzz/fuzz_tx fuzz/fuzz_block fuzz/fuzz_wtx fuzz/fuzz_logdb fuzz/fuzz_psbt +FUZZ_TARGETS = fuzz/fuzz_tx fuzz/fuzz_block fuzz/fuzz_wtx fuzz/fuzz_logdb fuzz/fuzz_psbt fuzz/fuzz_compact_block if WITH_NET FUZZ_TARGETS += fuzz/fuzz_protocol endif diff --git a/fuzz/fuzz_compact_block.c b/fuzz/fuzz_compact_block.c new file mode 100644 index 000000000..25b99e25d --- /dev/null +++ b/fuzz/fuzz_compact_block.c @@ -0,0 +1,109 @@ +/* + + The MIT License (MIT) + + Copyright (c) 2026 bluezr + Copyright (c) 2026 The Dogecoin Foundation + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +/* + * libFuzzer harness for the BIP152 compact block deserializers. + * + * All four parse peer-supplied bytes before any validation: + * + * cmpctblock header | nonce | vec | vec + * getblocktxn block_hash | vec + * blocktxn block_hash | vec + * sendcmpct announce (1) | version (8) + * + * cmpctblock and getblocktxn are the interesting pair. Both carry indices that + * are differentially encoded, so the parser accumulates a running total from + * attacker-controlled deltas -- the place an overflow or an out-of-range index + * would come from. cmpctblock additionally interleaves prefilled transactions + * with short IDs, so the two vectors have to stay consistent with each other. + * + * The first input byte selects the target so one corpus covers all four. + * + * Build: ./configure CC=clang CFLAGS="-fsanitize=fuzzer-no-link" --enable-fuzz && make fuzz + * Run: ./fuzz/fuzz_compact_block CORPUS_DIR -max_len=100000 + */ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 1) return 0; + + uint8_t selector = data[0]; + struct const_buffer buf = { data + 1, size - 1 }; + + switch (selector & 0x03) { + /* These _free functions release the container as well as its members, so + the structs have to come from their paired _new rather than the stack. + A stack struct here is an immediate ASAN bad-free -- which is how the + first version of this harness failed, two executions in. */ + case 0: { + dogecoin_compact_block *cmpctblk = dogecoin_compact_block_new(); + if (cmpctblk) { + dogecoin_compact_block_deserialize(cmpctblk, &buf, + &dogecoin_chainparams_main); + dogecoin_compact_block_free(cmpctblk); + } + break; + } + case 1: { + dogecoin_getblocktxn *req = dogecoin_getblocktxn_new(); + if (req) { + dogecoin_getblocktxn_deserialize(req, &buf); + dogecoin_getblocktxn_free(req); + } + break; + } + case 2: { + dogecoin_blocktxn *resp = dogecoin_blocktxn_new(); + if (resp) { + dogecoin_blocktxn_deserialize(resp, &buf); + dogecoin_blocktxn_free(resp); + } + break; + } + case 3: { + /* Cheap by comparison, but it is the message that decides whether + compact blocks are enabled at all and which version is negotiated, + so it is worth covering rather than assuming a 9-byte parser is + uninteresting. */ + dogecoin_bool high_bandwidth = false; + uint64_t version = 0; + dogecoin_p2p_msg_sendcmpct_deser(&high_bandwidth, &version, &buf); + break; + } + } + return 0; +} From e647a7375ddd38494288519208d230ade90a638c Mon Sep 17 00:00:00 2001 From: bluezr Date: Mon, 3 Aug 2026 11:32:16 -0700 Subject: [PATCH 7/7] fuzz: seed the compact block corpus with a real merge-mined header A cmpctblock header is 80 bytes plus a full AuxPoW whenever version bit 0x100 is set, so the harness needs an input where that blob is well formed, not merely present. The seed is a cmpctblock built from the height-371338 mainnet vector the BIP152 tests use: the 535-byte header span (80 base + 455 AuxPoW) measured by parsing it, followed by a known nonce and empty short-id and prefilled vectors. Same construction as test_compact_block_real_auxpow_header_roundtrip, so the fixture stays honest -- a synthetic AuxPoW would only prove the parser accepts what this repository invents. It is not about reaching the AuxPoW parser. That takes one version bit and the fuzzer finds it immediately: from an empty corpus it enters deserialize_dogecoin_auxpow_block 209105 times in 746529 runs. What random input does not produce is a *valid* AuxPoW, so those executions die in the first sub-parser. The seed gives the mutator a working structure to vary. Throughput shows the difference: 396923 runs seeded against 746529 unseeded over the same 60 seconds, roughly half the speed per execution, because inputs derived from the seed survive the early rejects and reach the parent coinbase, the merkle branches, the parent header, and check_auxpow's scrypt work. Placed under test/fuzz_corpus/compact_block/ to match the layout the PSBT harness established. --- .../compact_block/auxpow_cmpctblock_371338 | Bin 0 -> 546 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/fuzz_corpus/compact_block/auxpow_cmpctblock_371338 diff --git a/test/fuzz_corpus/compact_block/auxpow_cmpctblock_371338 b/test/fuzz_corpus/compact_block/auxpow_cmpctblock_371338 new file mode 100644 index 0000000000000000000000000000000000000000..c983d0f7040b16a7b4cf3227dd4954c4ef5abbb6 GIT binary patch literal 546 zcmZQzVoYKP{%|LNm;c5@HKn76+~b>rCaWEsyT)&7l=T0jKbtI#5^BG_Ywk`=5teAN z6p)umm_6mGh>f%C_c*TA`kS`7r<*1I77Uqi#XyweB(`*L#w z+do7{C*~L&oD%if_2SP!k7-sy7k(>7_f=iBQWul6;eZ-5`$t+zB#;f#%-F!gqXn`` zvTUWuD)WF<5<3=r{t(G-`0&?Glh(9`^BrqIx;8kiuh!z;U8?@aW4(Zwpn6`yPFbxp z5v4o!se+^#m{(RVGrO%4U}jl#NpRh>>OzYu+f|EMr62rNA!S@}*hIWV^n|S}kM6rSMK#I>iB;~I@<*Rf zVosYZbP;Sx{t{;P%}XqU8>gy7^FGbek=?e-D&@!COYE`?dpu4!Zhc-o