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 | |
| 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')
| -rw-r--r-- | src/Makefile | 24 | ||||
| -rw-r--r-- | src/compactblock.cpp | 155 | ||||
| -rw-r--r-- | src/compactblock.h | 113 | ||||
| -rw-r--r-- | src/test_compactblock.cpp | 133 |
4 files changed, 421 insertions, 4 deletions
diff --git a/src/Makefile b/src/Makefile index b931007..888110f 100644 --- a/src/Makefile +++ b/src/Makefile | |||
| @@ -55,7 +55,8 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ | |||
| 55 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 55 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ |
| 56 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ | 56 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ |
| 57 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ | 57 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ |
| 58 | selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h | 58 | selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h \ |
| 59 | compactblock.h | ||
| 59 | 60 | ||
| 60 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | 61 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ |
| 61 | obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o | 62 | obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o |
| @@ -64,7 +65,7 @@ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | |||
| 64 | obj/sha.o obj/nostr.o obj/randomx_pow.o \ | 65 | obj/sha.o obj/nostr.o obj/randomx_pow.o \ |
| 65 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ | 66 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ |
| 66 | obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ | 67 | obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ |
| 67 | obj/proxy.o obj/tor.o | 68 | obj/proxy.o obj/tor.o obj/compactblock.o |
| 68 | 69 | ||
| 69 | GUI_OBJS = obj/gui.o obj/main_gui.o | 70 | GUI_OBJS = obj/gui.o obj/main_gui.o |
| 70 | 71 | ||
| @@ -194,13 +195,28 @@ fuzz-script-smoke: fuzz-script | |||
| 194 | ./fuzz-script -runs=128 ../work/fuzz-corpus/script | 195 | ./fuzz-script -runs=128 ../work/fuzz-corpus/script |
| 195 | 196 | ||
| 196 | clean: | 197 | clean: |
| 197 | rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message fuzz-script test_bip32 | 198 | rm -rf obj obj-node obj-fuzz bitflash bitflash-node fuzz-net-message fuzz-script test_bip32 test_compactblock |
| 198 | 199 | ||
| 199 | test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h | 200 | test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h |
| 200 | $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto | 201 | $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto |
| 201 | 202 | ||
| 202 | tests: test_bip32 bitflash-node | 203 | # Upstream shipped test_compactblock.cpp with no build rule at all, so it was |
| 204 | # never compiled there. It links the standalone way test_bip32 does: it supplies | ||
| 205 | # its own mapTransactions and GetAppDir so main.cpp stays out of the link, and | ||
| 206 | # only needs sha.cpp/util.cpp for Hash() and the logging helpers. | ||
| 207 | # | ||
| 208 | # TEST_LIBS is overridable for the same reason NODE_LIBS is: the pinned dep tree | ||
| 209 | # ships libsodium as a static archive only, so a build against it passes | ||
| 210 | # TEST_LIBS="... -l:libsodium.a ...". | ||
| 211 | TEST_LIBS ?= -lsodium -lssl -lcrypto -lpthread | ||
| 212 | |||
| 213 | test_compactblock: test_compactblock.cpp compactblock.cpp compactblock.h $(HEADERS) | ||
| 214 | $(CXX) $(CXXFLAGS) -o $@ test_compactblock.cpp compactblock.cpp sha.cpp util.cpp \ | ||
| 215 | $(TEST_LIBS) | ||
| 216 | |||
| 217 | tests: test_bip32 test_compactblock bitflash-node | ||
| 203 | ./test_bip32 | 218 | ./test_bip32 |
| 219 | ./test_compactblock | ||
| 204 | ./bitflash-node -selftest=wallet-keypool | 220 | ./bitflash-node -selftest=wallet-keypool |
| 205 | ./bitflash-node -selftest=wallet-hd | 221 | ./bitflash-node -selftest=wallet-hd |
| 206 | ./bitflash-node -selftest=wallet-format | 222 | ./bitflash-node -selftest=wallet-format |
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 | } | ||
diff --git a/src/compactblock.h b/src/compactblock.h new file mode 100644 index 0000000..745ee3b --- /dev/null +++ b/src/compactblock.h | |||
| @@ -0,0 +1,113 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | // | ||
| 4 | // Compact block relay (BIP152-style). Instead of sending a peer's full set | ||
| 5 | // of transactions for a block they're about to receive -- almost all of | ||
| 6 | // which they already have in their mempool from normal tx relay -- send a | ||
| 7 | // short (48-bit, keyed-hash) ID per transaction instead. A peer that already | ||
| 8 | // has every referenced transaction reconstructs the full block locally from | ||
| 9 | // its mempool without any of the transaction bytes crossing the wire at | ||
| 10 | // all; only genuinely missing transactions (typically just the coinbase) | ||
| 11 | // need a follow-up round trip. | ||
| 12 | // | ||
| 13 | // Compatibility: this is purely additive. A peer that never sends | ||
| 14 | // "sendcmpct" is never sent a compact block -- it keeps getting full | ||
| 15 | // "block" messages exactly as before (see the getdata handler in | ||
| 16 | // main.cpp). Old, unupgraded nodes on the live network are completely | ||
| 17 | // unaffected; ProcessMessage's unknown-command handling means they simply | ||
| 18 | // never see the new message types. No coordinated network upgrade needed. | ||
| 19 | // | ||
| 20 | // Deliberately scoped down from full BIP152: no high-bandwidth (unsolicited | ||
| 21 | // push) mode, no differential-index encoding for prefilled transactions, | ||
| 22 | // only the current chain tip is ever offered in compact form (an old block | ||
| 23 | // is unlikely to still overlap much with any peer's current mempool, so the | ||
| 24 | // bandwidth win doesn't apply -- full "block" is sent for those, same as | ||
| 25 | // today). Only the coinbase is prefilled; every other transaction position | ||
| 26 | // carries a short ID. | ||
| 27 | |||
| 28 | #ifndef BITFLASH_COMPACTBLOCK_H | ||
| 29 | #define BITFLASH_COMPACTBLOCK_H | ||
| 30 | |||
| 31 | // Requires CBlock/CTransaction already visible -- #include "headers.h" (or | ||
| 32 | // main.h) before this file. Deliberately doesn't include main.h itself: | ||
| 33 | // main.h has no include guard (matches this codebase's existing headers, it | ||
| 34 | // relies on being pulled in exactly once via headers.h), so a second | ||
| 35 | // #include "main.h" here would break every translation unit that already | ||
| 36 | // brought it in via headers.h first. | ||
| 37 | #include <vector> | ||
| 38 | |||
| 39 | // The compact representation of a block: header fields, a per-block nonce | ||
| 40 | // (keys the short-ID hash so IDs can't be precomputed/ground across blocks), | ||
| 41 | // the coinbase in full, and a 48-bit short ID for every other transaction | ||
| 42 | // in block order. | ||
| 43 | class CCmpctBlock | ||
| 44 | { | ||
| 45 | public: | ||
| 46 | // header | ||
| 47 | int nVersion; | ||
| 48 | uint256 hashPrevBlock; | ||
| 49 | uint256 hashMerkleRoot; | ||
| 50 | unsigned int nTime; | ||
| 51 | unsigned int nBits; | ||
| 52 | unsigned int nNonce; | ||
| 53 | |||
| 54 | // Keys the short-ID hash for this block specifically -- chosen fresh | ||
| 55 | // per block (not reused from nNonce, which is public PoW data an | ||
| 56 | // attacker already knows and could otherwise grind against). | ||
| 57 | uint64 shortIdNonce; | ||
| 58 | |||
| 59 | // Sent in full. In this implementation: always exactly the coinbase, | ||
| 60 | // at index 0. | ||
| 61 | std::vector<unsigned int> vPrefilledIndex; | ||
| 62 | std::vector<CTransaction> vPrefilledTx; | ||
| 63 | |||
| 64 | // One short ID per NON-prefilled transaction, in ascending block-index | ||
| 65 | // order (i.e. the Nth entry here belongs to the Nth block position that | ||
| 66 | // isn't in vPrefilledIndex). | ||
| 67 | std::vector<uint64> vShortTxId; | ||
| 68 | |||
| 69 | IMPLEMENT_SERIALIZE | ||
| 70 | ( | ||
| 71 | READWRITE(this->nVersion); | ||
| 72 | nVersion = this->nVersion; | ||
| 73 | READWRITE(hashPrevBlock); | ||
| 74 | READWRITE(hashMerkleRoot); | ||
| 75 | READWRITE(nTime); | ||
| 76 | READWRITE(nBits); | ||
| 77 | READWRITE(nNonce); | ||
| 78 | READWRITE(shortIdNonce); | ||
| 79 | READWRITE(vPrefilledIndex); | ||
| 80 | READWRITE(vPrefilledTx); | ||
| 81 | READWRITE(vShortTxId); | ||
| 82 | ) | ||
| 83 | |||
| 84 | uint256 HeaderHash() const; | ||
| 85 | }; | ||
| 86 | |||
| 87 | // Compute the 48-bit short ID for a transaction under a given block's | ||
| 88 | // short-ID nonce. Keyed (via the block's hashPrevBlock + shortIdNonce) so | ||
| 89 | // IDs from one block can't be precomputed before that block exists, and | ||
| 90 | // differ block to block even for the same transaction. | ||
| 91 | uint64 ComputeShortTxId(const uint256& hashPrevBlock, uint64 shortIdNonce, const uint256& txid); | ||
| 92 | |||
| 93 | // Build the compact form of `block` (which must have vtx[0] as the | ||
| 94 | // coinbase, as every block on this chain does). | ||
| 95 | bool BuildCompactBlock(const CBlock& block, CCmpctBlock& out); | ||
| 96 | |||
| 97 | // Attempt to reconstruct a full block from a received compact block using | ||
| 98 | // the local mempool. On full success, returns true and fills `outBlock`. | ||
| 99 | // On partial success (some transactions not found locally), returns false | ||
| 100 | // and fills `vMissingIndex` with the block-position indices that still need | ||
| 101 | // to be requested via "getblocktxn" -- outBlock is left partially built | ||
| 102 | // (coinbase and any resolved positions filled; caller must not use it until | ||
| 103 | // FillMissingAndFinish completes it). | ||
| 104 | bool TryReconstructBlock(const CCmpctBlock& cmpct, CBlock& outBlock, std::vector<unsigned int>& vMissingIndex); | ||
| 105 | |||
| 106 | // Complete a partially-reconstructed block (from a prior TryReconstructBlock | ||
| 107 | // that returned false) using the transactions received in a "blocktxn" | ||
| 108 | // response. vTxn must be in the same order as the indices this node | ||
| 109 | // requested via "getblocktxn" for this block. | ||
| 110 | bool FillMissingAndFinish(CBlock& outBlock, const std::vector<unsigned int>& vMissingIndex, | ||
| 111 | const std::vector<CTransaction>& vTxn); | ||
| 112 | |||
| 113 | #endif | ||
diff --git a/src/test_compactblock.cpp b/src/test_compactblock.cpp new file mode 100644 index 0000000..f4b5583 --- /dev/null +++ b/src/test_compactblock.cpp | |||
| @@ -0,0 +1,133 @@ | |||
| 1 | // Standalone test of compactblock.cpp's core logic: build a compact block, | ||
| 2 | // reconstruct it from a simulated mempool, including the missing-tx path. | ||
| 3 | // Not wired into the P2P layer -- proves BuildCompactBlock/ | ||
| 4 | // TryReconstructBlock/FillMissingAndFinish are correct in isolation. | ||
| 5 | |||
| 6 | #include "headers.h" | ||
| 7 | #include "compactblock.h" | ||
| 8 | #include <cstdio> | ||
| 9 | #include <cassert> | ||
| 10 | |||
| 11 | // main.cpp defines this; we're not linking main.cpp, so define it here. | ||
| 12 | map<uint256, CTransaction> mapTransactions; | ||
| 13 | string GetAppDir() { return "."; } // util.h's debug-log path helper needs this | ||
| 14 | |||
| 15 | static int g_fail = 0; | ||
| 16 | #define CHECK(cond, msg) do { \ | ||
| 17 | if (cond) printf(" ok %s\n", msg); \ | ||
| 18 | else { printf(" FAIL %s\n", msg); g_fail++; } \ | ||
| 19 | } while (0) | ||
| 20 | |||
| 21 | // Build a syntactically-valid-enough CTransaction that GetHash()s uniquely | ||
| 22 | // per `nSeed` -- doesn't need to be consensus-valid, this test only | ||
| 23 | // exercises short-ID computation and reconstruction, not script validation. | ||
| 24 | static CTransaction MakeTx(int nSeed) | ||
| 25 | { | ||
| 26 | CTransaction tx; | ||
| 27 | tx.nVersion = 1; | ||
| 28 | tx.nLockTime = nSeed; // cheapest way to make each tx's serialization (and hash) unique | ||
| 29 | return tx; | ||
| 30 | } | ||
| 31 | |||
| 32 | int main() | ||
| 33 | { | ||
| 34 | // Build a block: coinbase + 20 regular transactions. | ||
| 35 | CBlock block; | ||
| 36 | block.nVersion = 1; | ||
| 37 | block.hashPrevBlock = uint256("0x00000000000000000000000000000000000000000000000000000000000abc"); | ||
| 38 | block.nTime = 1234567890; | ||
| 39 | block.nBits = 0x1f0fffff; | ||
| 40 | block.nNonce = 42; | ||
| 41 | block.vtx.push_back(MakeTx(0)); // coinbase | ||
| 42 | for (int i = 1; i <= 20; i++) | ||
| 43 | block.vtx.push_back(MakeTx(i)); | ||
| 44 | block.hashMerkleRoot = block.BuildMerkleTree(); | ||
| 45 | |||
| 46 | printf("build_and_reconstruct_full_mempool\n"); | ||
| 47 | { | ||
| 48 | CCmpctBlock cmpct; | ||
| 49 | CHECK(BuildCompactBlock(block, cmpct), "BuildCompactBlock succeeds"); | ||
| 50 | CHECK(cmpct.vPrefilledIndex.size() == 1 && cmpct.vPrefilledIndex[0] == 0, "only the coinbase is prefilled"); | ||
| 51 | CHECK(cmpct.vShortTxId.size() == 20, "one short ID per remaining tx"); | ||
| 52 | |||
| 53 | // Simulate a peer with every transaction already in its mempool. | ||
| 54 | mapTransactions.clear(); | ||
| 55 | for (size_t i = 1; i < block.vtx.size(); i++) | ||
| 56 | mapTransactions[block.vtx[i].GetHash()] = block.vtx[i]; | ||
| 57 | |||
| 58 | CBlock rebuilt; | ||
| 59 | vector<unsigned int> vMissing; | ||
| 60 | CHECK(TryReconstructBlock(cmpct, rebuilt, vMissing), "fully reconstructs when mempool has everything"); | ||
| 61 | CHECK(vMissing.empty(), "nothing reported missing"); | ||
| 62 | CHECK(rebuilt.GetHash() == block.GetHash(), "reconstructed block hash matches original exactly"); | ||
| 63 | CHECK(rebuilt.vtx.size() == block.vtx.size(), "same transaction count"); | ||
| 64 | bool fAllMatch = true; | ||
| 65 | for (size_t i = 0; i < block.vtx.size(); i++) | ||
| 66 | if (rebuilt.vtx[i].GetHash() != block.vtx[i].GetHash()) fAllMatch = false; | ||
| 67 | CHECK(fAllMatch, "every transaction in the right position"); | ||
| 68 | } | ||
| 69 | |||
| 70 | printf("partial_mempool_then_getblocktxn_completes_it\n"); | ||
| 71 | { | ||
| 72 | CCmpctBlock cmpct; | ||
| 73 | BuildCompactBlock(block, cmpct); | ||
| 74 | |||
| 75 | // Simulate a peer missing 3 of the 20 non-coinbase transactions. | ||
| 76 | mapTransactions.clear(); | ||
| 77 | vector<unsigned int> expectMissing; | ||
| 78 | for (size_t i = 1; i < block.vtx.size(); i++) | ||
| 79 | { | ||
| 80 | if (i == 5 || i == 12 || i == 19) { expectMissing.push_back((unsigned int)i); continue; } | ||
| 81 | mapTransactions[block.vtx[i].GetHash()] = block.vtx[i]; | ||
| 82 | } | ||
| 83 | |||
| 84 | CBlock rebuilt; | ||
| 85 | vector<unsigned int> vMissing; | ||
| 86 | CHECK(!TryReconstructBlock(cmpct, rebuilt, vMissing), "reports incomplete when mempool is missing some tx"); | ||
| 87 | CHECK(vMissing == expectMissing, "missing indices are exactly the ones not in mempool"); | ||
| 88 | |||
| 89 | // Simulate the getblocktxn round trip: peer sends back the missing txs. | ||
| 90 | vector<CTransaction> vTxn; | ||
| 91 | foreach(unsigned int idx, vMissing) | ||
| 92 | vTxn.push_back(block.vtx[idx]); | ||
| 93 | |||
| 94 | CHECK(FillMissingAndFinish(rebuilt, vMissing, vTxn), "FillMissingAndFinish succeeds with matching response"); | ||
| 95 | CHECK(rebuilt.GetHash() == block.GetHash(), "block hash matches after filling in the missing tx"); | ||
| 96 | } | ||
| 97 | |||
| 98 | printf("mismatched_blocktxn_response_rejected\n"); | ||
| 99 | { | ||
| 100 | CCmpctBlock cmpct; | ||
| 101 | BuildCompactBlock(block, cmpct); | ||
| 102 | mapTransactions.clear(); | ||
| 103 | mapTransactions[block.vtx[1].GetHash()] = block.vtx[1]; // only one in mempool | ||
| 104 | |||
| 105 | CBlock rebuilt; | ||
| 106 | vector<unsigned int> vMissing; | ||
| 107 | TryReconstructBlock(cmpct, rebuilt, vMissing); | ||
| 108 | CHECK(vMissing.size() == 19, "19 missing when mempool has only 1 of 20"); | ||
| 109 | |||
| 110 | vector<CTransaction> vWrongCount; | ||
| 111 | vWrongCount.push_back(block.vtx[2]); // way fewer than vMissing.size() | ||
| 112 | CHECK(!FillMissingAndFinish(rebuilt, vMissing, vWrongCount), "rejects a response with the wrong tx count"); | ||
| 113 | } | ||
| 114 | |||
| 115 | printf("different_blocks_give_different_short_ids_for_same_tx\n"); | ||
| 116 | { | ||
| 117 | // Same transaction, two different (simulated) blocks -- short IDs | ||
| 118 | // must differ, since they're keyed per-block (hashPrevBlock + | ||
| 119 | // fresh nonce each time), not just derived from the txid alone. | ||
| 120 | CTransaction tx = MakeTx(99); | ||
| 121 | uint256 prevA = uint256("0x1111111111111111111111111111111111111111111111111111111111111111"); | ||
| 122 | uint256 prevB = uint256("0x2222222222222222222222222222222222222222222222222222222222222222"); | ||
| 123 | uint64 id1 = ComputeShortTxId(prevA, 0xAAAABBBBCCCCDDDDULL, tx.GetHash()); | ||
| 124 | uint64 id2 = ComputeShortTxId(prevB, 0xAAAABBBBCCCCDDDDULL, tx.GetHash()); | ||
| 125 | uint64 id3 = ComputeShortTxId(prevA, 0x1111222233334444ULL, tx.GetHash()); | ||
| 126 | CHECK(id1 != id2, "different hashPrevBlock -> different short ID"); | ||
| 127 | CHECK(id1 != id3, "different nonce -> different short ID"); | ||
| 128 | CHECK(id1 <= 0xFFFFFFFFFFFFULL, "short ID fits in 48 bits"); | ||
| 129 | } | ||
| 130 | |||
| 131 | printf("\n%s (%d failures)\n", g_fail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", g_fail); | ||
| 132 | return g_fail == 0 ? 0 : 1; | ||
| 133 | } | ||