diff --git a/CMakeLists.txt b/CMakeLists.txt index 72dc941aa..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 diff --git a/Makefile.am b/Makefile.am index 14dc6d8ca..7d6ee93e8 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 \ @@ -369,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 \ @@ -503,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 @@ -559,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; +} diff --git a/include/dogecoin/compact_block.h b/include/dogecoin/compact_block.h new file mode 100644 index 000000000..b65e48d07 --- /dev/null +++ b/include/dogecoin/compact_block.h @@ -0,0 +1,474 @@ +/* + + 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 + +/* 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 + +/* ================================================================ */ +/* 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; /**< 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) */ + 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 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. + * + * 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 The parsed block header. + * @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. + * + * 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 dogecoin_bool 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. + */ +/** + * @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, + 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/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 new file mode 100644 index 000000000..faab07106 --- /dev/null +++ b/src/compact_block.c @@ -0,0 +1,808 @@ +/* + + 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 +#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->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; + } + + 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 */ +/* ================================================================ */ + +/** + * @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_raw( + const uint8_t *header_ser, + size_t header_ser_len, + uint64_t nonce, + uint64_t *k0_out, + uint64_t *k1_out) +{ + 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(preimage, nonce); + + /* SHA256(header || nonce) - single SHA256 per BIP152 */ + uint256_t hash; + 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; + *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); +} + +/** + * 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. + */ +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 */ +/* ================================================================ */ + +dogecoin_bool dogecoin_compact_block_serialize(cstring *s, const dogecoin_compact_block *cmpctblk) +{ + 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); + + /* 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; + } + + return true; +} + +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. 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 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)) + 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 + +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, + 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); + 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); + 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; + + /* 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; + + /* 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; + + 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) 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; + 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) { + 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) goto fail; + 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); + +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( + dogecoin_compact_block_state *state, + 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 */ + 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/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)) { 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 new file mode 100644 index 000000000..51dc8137e --- /dev/null +++ b/test/compact_block_tests.c @@ -0,0 +1,471 @@ +/* + + 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 +#include +#include + +#include "data/auxpow_block_371338.h" + +/* ================================================================ */ +/* 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 */ +/* ================================================================ */ + +/* 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(); + 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(); + 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__ */ 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 000000000..c983d0f70 Binary files /dev/null and b/test/fuzz_corpus/compact_block/auxpow_cmpctblock_371338 differ 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);