From 60538c5a935787521a5ad8c2a7dce52b5f250f3a Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Sat, 5 Sep 2026 03:16:00 +0000 Subject: compactblock: import BIP152-style compact block relay from upstream 1.2.23 Brings src/compactblock.{cpp,h} and test_compactblock.cpp across verbatim from the upstream-v1.2.23 tag, and gives them a build. 1.2.23 is not a newer release of the line this fork ships. It is a lightweight tag (7ab5a8c) cut 2026-07-30, on a lineage that upstream/main (e0be70e, 2026-08-20) does not contain and no remote branch carries -- 272 commits that are not ours, against 376 of ours that are not theirs, from a merge-base at 56df983. Merging that tag would have deleted 18 files from src/, selftest.{cpp,h} and the whole 14-test suite among them, so the content is ported file by file instead. Compact block relay is the piece worth having: a peer that advertises sendcmpct gets 48-bit keyed short IDs per transaction rather than the transaction bytes, and reconstructs the block from its own mempool. It is purely additive on the wire -- a peer that never sends sendcmpct keeps receiving full block messages, so no coordinated upgrade is needed. This commit is the primitives and their unit test only. Nothing calls BuildCompactBlock or TryReconstructBlock yet: the P2P message handlers (sendcmpct/cmpctblock/getblocktxn/blocktxn) live in 1.2.23's main.cpp and are deliberately left for a follow-up, so that no attacker-controlled bytes reach this code until that wiring is reviewed on its own. The files needed no API adaptation -- mapTransactions, BEGIN/END and IMPLEMENT_SERIALIZE are unchanged across the divergence, and it compiled clean first time. Upstream never wired test_compactblock.cpp into its own Makefile, so it had never been built there; it links standalone the way test_bip32 does, supplying its own mapTransactions and GetAppDir to keep main.cpp out of the link. TEST_LIBS is overridable for the same reason NODE_LIBS is: the pinned dep tree ships libsodium as a static archive only. All 17 checks in test_compactblock pass, all 14 selftests still pass, and the consensus constants are unchanged against e4f898c. --- src/compactblock.cpp | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/compactblock.cpp (limited to 'src/compactblock.cpp') diff --git a/src/compactblock.cpp b/src/compactblock.cpp new file mode 100644 index 0000000..3999ab7 --- /dev/null +++ b/src/compactblock.cpp @@ -0,0 +1,155 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// See compactblock.h. + +#include "headers.h" +#include "compactblock.h" +#include +#include + +uint256 CCmpctBlock::HeaderHash() const +{ + // Same technique CBlock::GetHash() uses: the header fields are laid out + // contiguously and in the same order here as in CBlock, so hashing the + // raw member range gives the identical block-identity hash. + return Hash(BEGIN(nVersion), END(nNonce)); +} + +uint64 ComputeShortTxId(const uint256& hashPrevBlock, uint64 shortIdNonce, const uint256& txid) +{ + // Derive a 16-byte SipHash key from (hashPrevBlock, shortIdNonce), so + // short IDs are specific to this block: they can't be precomputed + // before the block (and its freshly-chosen nonce) exist, and the same + // transaction gets a different short ID in every block it appears in + // (relevant across reorgs/re-announcements). + unsigned char keyMaterial[32 + sizeof(uint64)]; + memcpy(keyMaterial, BEGIN(hashPrevBlock), 32); + memcpy(keyMaterial + 32, &shortIdNonce, sizeof(uint64)); + uint256 keyHash = Hash((const char*)keyMaterial, (const char*)keyMaterial + sizeof(keyMaterial)); + + unsigned char out[crypto_shorthash_BYTES]; // 8 bytes + crypto_shorthash(out, (const unsigned char*)BEGIN(txid), 32, (const unsigned char*)BEGIN(keyHash)); + + uint64 id = 0; + memcpy(&id, out, sizeof(uint64)); + return id & 0xFFFFFFFFFFFFULL; // low 48 bits -- matches the wire field width +} + +bool BuildCompactBlock(const CBlock& block, CCmpctBlock& out) +{ + if (block.vtx.empty()) + return false; // every real block has at least the coinbase + + out.nVersion = block.nVersion; + out.hashPrevBlock = block.hashPrevBlock; + out.hashMerkleRoot = block.hashMerkleRoot; + out.nTime = block.nTime; + out.nBits = block.nBits; + out.nNonce = block.nNonce; + + unsigned char nonceBytes[sizeof(uint64)]; + RAND_bytes(nonceBytes, sizeof(nonceBytes)); + memcpy(&out.shortIdNonce, nonceBytes, sizeof(uint64)); + + out.vPrefilledIndex.clear(); + out.vPrefilledTx.clear(); + out.vShortTxId.clear(); + + // Only the coinbase is prefilled (index 0) -- see file comment in + // compactblock.h for why that's enough for this chain's scale. + out.vPrefilledIndex.push_back(0); + out.vPrefilledTx.push_back(block.vtx[0]); + + for (size_t i = 1; i < block.vtx.size(); i++) + out.vShortTxId.push_back(ComputeShortTxId(out.hashPrevBlock, out.shortIdNonce, block.vtx[i].GetHash())); + + return true; +} + +bool TryReconstructBlock(const CCmpctBlock& cmpct, CBlock& outBlock, std::vector& vMissingIndex) +{ + vMissingIndex.clear(); + + size_t nTotal = cmpct.vPrefilledIndex.size() + cmpct.vShortTxId.size(); + if (cmpct.vPrefilledIndex.size() != cmpct.vPrefilledTx.size() || nTotal == 0) + return false; // malformed + + outBlock.SetNull(); + outBlock.nVersion = cmpct.nVersion; + outBlock.hashPrevBlock = cmpct.hashPrevBlock; + outBlock.hashMerkleRoot = cmpct.hashMerkleRoot; + outBlock.nTime = cmpct.nTime; + outBlock.nBits = cmpct.nBits; + outBlock.nNonce = cmpct.nNonce; + outBlock.vtx.assign(nTotal, CTransaction()); + + std::vector vFilled(nTotal, false); + for (size_t i = 0; i < cmpct.vPrefilledIndex.size(); i++) + { + unsigned int idx = cmpct.vPrefilledIndex[i]; + if (idx >= nTotal) + return false; // malformed + outBlock.vtx[idx] = cmpct.vPrefilledTx[i]; + vFilled[idx] = true; + } + + // Index every mempool tx by its short ID under this block's keying, so + // resolving each block position is a map lookup rather than a linear + // scan of the mempool per short ID. + std::map mapShortIdToTx; + for (std::map::const_iterator mi = mapTransactions.begin(); + mi != mapTransactions.end(); ++mi) + { + uint64 sid = ComputeShortTxId(cmpct.hashPrevBlock, cmpct.shortIdNonce, mi->first); + // A short-ID collision between two distinct mempool transactions is + // possible (48 bits, birthday bound ~2^24 mempool entries) though + // very unlikely at this chain's scale; on collision, prefer keeping + // neither mapped rather than risk resolving to the wrong tx -- + // that position just falls through to getblocktxn like a genuine + // miss, which is always correct, only occasionally not optimal. + if (mapShortIdToTx.count(sid)) + mapShortIdToTx[sid] = NULL; + else + mapShortIdToTx[sid] = &mi->second; + } + + size_t shortIdPos = 0; + for (size_t idx = 0; idx < nTotal; idx++) + { + if (vFilled[idx]) + continue; + if (shortIdPos >= cmpct.vShortTxId.size()) + return false; // malformed: more unfilled positions than short IDs + uint64 sid = cmpct.vShortTxId[shortIdPos++]; + + std::map::const_iterator found = mapShortIdToTx.find(sid); + if (found != mapShortIdToTx.end() && found->second != NULL) + { + outBlock.vtx[idx] = *found->second; + vFilled[idx] = true; + } + else + { + vMissingIndex.push_back(idx); + } + } + + return vMissingIndex.empty(); +} + +bool FillMissingAndFinish(CBlock& outBlock, const std::vector& vMissingIndex, + const std::vector& vTxn) +{ + if (vMissingIndex.size() != vTxn.size()) + return false; // peer didn't answer what was asked + + for (size_t i = 0; i < vMissingIndex.size(); i++) + { + unsigned int idx = vMissingIndex[i]; + if (idx >= outBlock.vtx.size()) + return false; // malformed + outBlock.vtx[idx] = vTxn[i]; + } + return true; +} -- cgit v1.2.3 From 36201d2795b7b49beb1a837b0252134096beb64d Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Sat, 5 Sep 2026 03:16:12 +0000 Subject: compactblock: fail closed on RAND_bytes error, bound reconstruction allocation Two fixes to the file imported in the previous commit. Kept separate so the deviation from upstream's bytes is reviewable on its own. BuildCompactBlock ignored the RAND_bytes return value. On failure nonceBytes is left uninitialised and an indeterminate shortIdNonce is memcpy'd out and sent on the wire -- and that nonce is exactly what the file relies on being unpredictable so short IDs cannot be precomputed before a block exists or ground across blocks. Every other RAND_bytes call site in this tree (bip32.cpp:203, db.cpp:2462, db.cpp:2469) checks for != 1 and fails closed; this one now does too. The caller falls back to full-block relay. TryReconstructBlock sized a vector directly from the wire. nTotal is the sum of two deserialised vector lengths, and serialize.h bounds a vector only by MAX_SIZE (0x02000000), so once the P2P handlers are wired a single small message could ask this node to default-construct ~33M CTransactions. The smallest a transaction can serialise to is 10 bytes, so no block within MAX_BLOCK_SIZE holds more than MAX_BLOCK_SIZE/10 of them; anything above that is not a block this node could accept under any circumstance. Rejecting falls back to full-block relay, which is always correct. Neither is reachable today -- nothing feeds these functions from the network yet -- but both are latent the moment the message handlers land, which is the wrong time to be finding them. test_compactblock (17 checks) and all 14 selftests pass with both in place. --- src/compactblock.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) (limited to 'src/compactblock.cpp') diff --git a/src/compactblock.cpp b/src/compactblock.cpp index 3999ab7..1ca6118 100644 --- a/src/compactblock.cpp +++ b/src/compactblock.cpp @@ -48,8 +48,16 @@ bool BuildCompactBlock(const CBlock& block, CCmpctBlock& out) out.nBits = block.nBits; out.nNonce = block.nNonce; + // Upstream ignored the RAND_bytes return value here. On failure nonceBytes + // is left uninitialised and an indeterminate shortIdNonce goes out on the + // wire -- which is precisely the value this file relies on being + // unpredictable to stop short IDs being precomputed or ground against. + // Fail closed instead, matching every other RAND_bytes call site in this + // tree (bip32.cpp, db.cpp): the caller falls back to full-block relay, + // which is always correct. unsigned char nonceBytes[sizeof(uint64)]; - RAND_bytes(nonceBytes, sizeof(nonceBytes)); + if (RAND_bytes(nonceBytes, sizeof(nonceBytes)) != 1) + return false; memcpy(&out.shortIdNonce, nonceBytes, sizeof(uint64)); out.vPrefilledIndex.clear(); @@ -75,6 +83,18 @@ bool TryReconstructBlock(const CCmpctBlock& cmpct, CBlock& outBlock, std::vector if (cmpct.vPrefilledIndex.size() != cmpct.vPrefilledTx.size() || nTotal == 0) return false; // malformed + // nTotal comes straight off the wire once this is wired into the P2P + // layer, and serialize.h only bounds a vector length by MAX_SIZE + // (0x02000000), so an unbounded assign() below would let one 60-odd-byte + // message ask this node for ~33M default-constructed CTransactions. The + // smallest a transaction can serialise to is 10 bytes (nVersion, empty + // vin, empty vout, nLockTime), so no block within MAX_BLOCK_SIZE can hold + // more than MAX_BLOCK_SIZE/10 of them; anything above that is not a block + // we could ever accept. Rejecting here just falls back to full-block + // relay, which is always correct. + if (nTotal > MAX_BLOCK_SIZE / 10) + return false; // implausible tx count -- refuse to size a vector from it + outBlock.SetNull(); outBlock.nVersion = cmpct.nVersion; outBlock.hashPrevBlock = cmpct.hashPrevBlock; -- cgit v1.2.3