upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile24
-rw-r--r--src/compactblock.cpp155
-rw-r--r--src/compactblock.h113
-rw-r--r--src/test_compactblock.cpp133
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 \
55HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 55HEADERS = 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
60IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 61IMGUI_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
69GUI_OBJS = obj/gui.o obj/main_gui.o 70GUI_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
196clean: 197clean:
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
199test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h 200test_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
202tests: 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 ...".
211TEST_LIBS ?= -lsodium -lssl -lcrypto -lpthread
212
213test_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
217tests: 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
11uint256 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
19uint64 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
39bool 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
70bool 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
141bool 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.
43class CCmpctBlock
44{
45public:
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.
91uint64 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).
95bool 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).
104bool 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.
110bool 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.
12map<uint256, CTransaction> mapTransactions;
13string GetAppDir() { return "."; } // util.h's debug-log path helper needs this
14
15static 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.
24static 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
32int 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}