diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 03:16:00 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 03:16:00 +0000 |
| commit | 60538c5a935787521a5ad8c2a7dce52b5f250f3a (patch) | |
| tree | b78632a9d3b15b7a99567fe3c213d0bb00fd1731 /src/compactblock.cpp | |
| parent | 152c859b18b222a7c11a82b5485110e902267d40 (diff) | |
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.
Diffstat (limited to 'src/compactblock.cpp')
| -rw-r--r-- | src/compactblock.cpp | 155 |
1 files changed, 155 insertions, 0 deletions
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 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | // | ||
| 4 | // See compactblock.h. | ||
| 5 | |||
| 6 | #include "headers.h" | ||
| 7 | #include "compactblock.h" | ||
| 8 | #include <sodium.h> | ||
| 9 | #include <map> | ||
| 10 | |||
| 11 | uint256 CCmpctBlock::HeaderHash() const | ||
| 12 | { | ||
| 13 | // Same technique CBlock::GetHash() uses: the header fields are laid out | ||
| 14 | // contiguously and in the same order here as in CBlock, so hashing the | ||
| 15 | // raw member range gives the identical block-identity hash. | ||
| 16 | return Hash(BEGIN(nVersion), END(nNonce)); | ||
| 17 | } | ||
| 18 | |||
| 19 | uint64 ComputeShortTxId(const uint256& hashPrevBlock, uint64 shortIdNonce, const uint256& txid) | ||
| 20 | { | ||
| 21 | // Derive a 16-byte SipHash key from (hashPrevBlock, shortIdNonce), so | ||
| 22 | // short IDs are specific to this block: they can't be precomputed | ||
| 23 | // before the block (and its freshly-chosen nonce) exist, and the same | ||
| 24 | // transaction gets a different short ID in every block it appears in | ||
| 25 | // (relevant across reorgs/re-announcements). | ||
| 26 | unsigned char keyMaterial[32 + sizeof(uint64)]; | ||
| 27 | memcpy(keyMaterial, BEGIN(hashPrevBlock), 32); | ||
| 28 | memcpy(keyMaterial + 32, &shortIdNonce, sizeof(uint64)); | ||
| 29 | uint256 keyHash = Hash((const char*)keyMaterial, (const char*)keyMaterial + sizeof(keyMaterial)); | ||
| 30 | |||
| 31 | unsigned char out[crypto_shorthash_BYTES]; // 8 bytes | ||
| 32 | crypto_shorthash(out, (const unsigned char*)BEGIN(txid), 32, (const unsigned char*)BEGIN(keyHash)); | ||
| 33 | |||
| 34 | uint64 id = 0; | ||
| 35 | memcpy(&id, out, sizeof(uint64)); | ||
| 36 | return id & 0xFFFFFFFFFFFFULL; // low 48 bits -- matches the wire field width | ||
| 37 | } | ||
| 38 | |||
| 39 | bool BuildCompactBlock(const CBlock& block, CCmpctBlock& out) | ||
| 40 | { | ||
| 41 | if (block.vtx.empty()) | ||
| 42 | return false; // every real block has at least the coinbase | ||
| 43 | |||
| 44 | out.nVersion = block.nVersion; | ||
| 45 | out.hashPrevBlock = block.hashPrevBlock; | ||
| 46 | out.hashMerkleRoot = block.hashMerkleRoot; | ||
| 47 | out.nTime = block.nTime; | ||
| 48 | out.nBits = block.nBits; | ||
| 49 | out.nNonce = block.nNonce; | ||
| 50 | |||
| 51 | unsigned char nonceBytes[sizeof(uint64)]; | ||
| 52 | RAND_bytes(nonceBytes, sizeof(nonceBytes)); | ||
| 53 | memcpy(&out.shortIdNonce, nonceBytes, sizeof(uint64)); | ||
| 54 | |||
| 55 | out.vPrefilledIndex.clear(); | ||
| 56 | out.vPrefilledTx.clear(); | ||
| 57 | out.vShortTxId.clear(); | ||
| 58 | |||
| 59 | // Only the coinbase is prefilled (index 0) -- see file comment in | ||
| 60 | // compactblock.h for why that's enough for this chain's scale. | ||
| 61 | out.vPrefilledIndex.push_back(0); | ||
| 62 | out.vPrefilledTx.push_back(block.vtx[0]); | ||
| 63 | |||
| 64 | for (size_t i = 1; i < block.vtx.size(); i++) | ||
| 65 | out.vShortTxId.push_back(ComputeShortTxId(out.hashPrevBlock, out.shortIdNonce, block.vtx[i].GetHash())); | ||
| 66 | |||
| 67 | return true; | ||
| 68 | } | ||
| 69 | |||
| 70 | bool TryReconstructBlock(const CCmpctBlock& cmpct, CBlock& outBlock, std::vector<unsigned int>& vMissingIndex) | ||
| 71 | { | ||
| 72 | vMissingIndex.clear(); | ||
| 73 | |||
| 74 | size_t nTotal = cmpct.vPrefilledIndex.size() + cmpct.vShortTxId.size(); | ||
| 75 | if (cmpct.vPrefilledIndex.size() != cmpct.vPrefilledTx.size() || nTotal == 0) | ||
| 76 | return false; // malformed | ||
| 77 | |||
| 78 | outBlock.SetNull(); | ||
| 79 | outBlock.nVersion = cmpct.nVersion; | ||
| 80 | outBlock.hashPrevBlock = cmpct.hashPrevBlock; | ||
| 81 | outBlock.hashMerkleRoot = cmpct.hashMerkleRoot; | ||
| 82 | outBlock.nTime = cmpct.nTime; | ||
| 83 | outBlock.nBits = cmpct.nBits; | ||
| 84 | outBlock.nNonce = cmpct.nNonce; | ||
| 85 | outBlock.vtx.assign(nTotal, CTransaction()); | ||
| 86 | |||
| 87 | std::vector<bool> vFilled(nTotal, false); | ||
| 88 | for (size_t i = 0; i < cmpct.vPrefilledIndex.size(); i++) | ||
| 89 | { | ||
| 90 | unsigned int idx = cmpct.vPrefilledIndex[i]; | ||
| 91 | if (idx >= nTotal) | ||
| 92 | return false; // malformed | ||
| 93 | outBlock.vtx[idx] = cmpct.vPrefilledTx[i]; | ||
| 94 | vFilled[idx] = true; | ||
| 95 | } | ||
| 96 | |||
| 97 | // Index every mempool tx by its short ID under this block's keying, so | ||
| 98 | // resolving each block position is a map lookup rather than a linear | ||
| 99 | // scan of the mempool per short ID. | ||
| 100 | std::map<uint64, const CTransaction*> mapShortIdToTx; | ||
| 101 | for (std::map<uint256, CTransaction>::const_iterator mi = mapTransactions.begin(); | ||
| 102 | mi != mapTransactions.end(); ++mi) | ||
| 103 | { | ||
| 104 | uint64 sid = ComputeShortTxId(cmpct.hashPrevBlock, cmpct.shortIdNonce, mi->first); | ||
| 105 | // A short-ID collision between two distinct mempool transactions is | ||
| 106 | // possible (48 bits, birthday bound ~2^24 mempool entries) though | ||
| 107 | // very unlikely at this chain's scale; on collision, prefer keeping | ||
| 108 | // neither mapped rather than risk resolving to the wrong tx -- | ||
| 109 | // that position just falls through to getblocktxn like a genuine | ||
| 110 | // miss, which is always correct, only occasionally not optimal. | ||
| 111 | if (mapShortIdToTx.count(sid)) | ||
| 112 | mapShortIdToTx[sid] = NULL; | ||
| 113 | else | ||
| 114 | mapShortIdToTx[sid] = &mi->second; | ||
| 115 | } | ||
| 116 | |||
| 117 | size_t shortIdPos = 0; | ||
| 118 | for (size_t idx = 0; idx < nTotal; idx++) | ||
| 119 | { | ||
| 120 | if (vFilled[idx]) | ||
| 121 | continue; | ||
| 122 | if (shortIdPos >= cmpct.vShortTxId.size()) | ||
| 123 | return false; // malformed: more unfilled positions than short IDs | ||
| 124 | uint64 sid = cmpct.vShortTxId[shortIdPos++]; | ||
| 125 | |||
| 126 | std::map<uint64, const CTransaction*>::const_iterator found = mapShortIdToTx.find(sid); | ||
| 127 | if (found != mapShortIdToTx.end() && found->second != NULL) | ||
| 128 | { | ||
| 129 | outBlock.vtx[idx] = *found->second; | ||
| 130 | vFilled[idx] = true; | ||
| 131 | } | ||
| 132 | else | ||
| 133 | { | ||
| 134 | vMissingIndex.push_back(idx); | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | return vMissingIndex.empty(); | ||
| 139 | } | ||
| 140 | |||
| 141 | bool FillMissingAndFinish(CBlock& outBlock, const std::vector<unsigned int>& vMissingIndex, | ||
| 142 | const std::vector<CTransaction>& vTxn) | ||
| 143 | { | ||
| 144 | if (vMissingIndex.size() != vTxn.size()) | ||
| 145 | return false; // peer didn't answer what was asked | ||
| 146 | |||
| 147 | for (size_t i = 0; i < vMissingIndex.size(); i++) | ||
| 148 | { | ||
| 149 | unsigned int idx = vMissingIndex[i]; | ||
| 150 | if (idx >= outBlock.vtx.size()) | ||
| 151 | return false; // malformed | ||
| 152 | outBlock.vtx[idx] = vTxn[i]; | ||
| 153 | } | ||
| 154 | return true; | ||
| 155 | } | ||