upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/compactblock.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
commitceae3d076b5ad11ee84729ae43f14e9312896646 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/compactblock.cpp
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parent66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff)
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git merge-base returns no result at all -- so anyone cloning this repository landed on a default branch that was neither current nor related to the running node. The 28 commits on main were audited before this merge: every one is already present in the tree by content, including the two that are hardest to see because they were rewritten afterwards -- the payout reorg-safety work (PayoutBlockStillOnChain, since evolved to re-check on every retry rather than once at maturity) and the nostr relay certificate verification (SSL_set1_host, now with a Windows root-store path). Nothing on main is lost by this merge. Recorded as a merge rather than a force-push: main only ever moves forward, its old history stays reachable as the first parent, and this is revertible. The tree is taken wholesale from the merged branch, so main's content is now exactly what was built and tested. What this brings onto main: - compactblock: BIP152-style compact block relay ported from upstream 1.2.23, primitives and unit test only, with the RAND_bytes failure path closed and the reconstruction allocation bounded. - upstream/main 17741d9: isolated testnet parameters and genesis, optional 2 MB large pages for RandomX. Mainnet genesis verified byte-identical through upstream's BuildGenesisBlock() refactor. - deploy: every image built on bookworm so binaries run on Debian 12, and the container build now honours the RandomX and libsecp256k1 pins it had been silently ignoring since before 1.2.15. Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus constants unchanged against e4f898c apart from the pre-existing MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'src/compactblock.cpp')
-rw-r--r--src/compactblock.cpp175
1 files changed, 175 insertions, 0 deletions
diff --git a/src/compactblock.cpp b/src/compactblock.cpp
new file mode 100644
index 0000000..1ca6118
--- /dev/null
+++ b/src/compactblock.cpp
@@ -0,0 +1,175 @@
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 // Upstream ignored the RAND_bytes return value here. On failure nonceBytes
52 // is left uninitialised and an indeterminate shortIdNonce goes out on the
53 // wire -- which is precisely the value this file relies on being
54 // unpredictable to stop short IDs being precomputed or ground against.
55 // Fail closed instead, matching every other RAND_bytes call site in this
56 // tree (bip32.cpp, db.cpp): the caller falls back to full-block relay,
57 // which is always correct.
58 unsigned char nonceBytes[sizeof(uint64)];
59 if (RAND_bytes(nonceBytes, sizeof(nonceBytes)) != 1)
60 return false;
61 memcpy(&out.shortIdNonce, nonceBytes, sizeof(uint64));
62
63 out.vPrefilledIndex.clear();
64 out.vPrefilledTx.clear();
65 out.vShortTxId.clear();
66
67 // Only the coinbase is prefilled (index 0) -- see file comment in
68 // compactblock.h for why that's enough for this chain's scale.
69 out.vPrefilledIndex.push_back(0);
70 out.vPrefilledTx.push_back(block.vtx[0]);
71
72 for (size_t i = 1; i < block.vtx.size(); i++)
73 out.vShortTxId.push_back(ComputeShortTxId(out.hashPrevBlock, out.shortIdNonce, block.vtx[i].GetHash()));
74
75 return true;
76}
77
78bool TryReconstructBlock(const CCmpctBlock& cmpct, CBlock& outBlock, std::vector<unsigned int>& vMissingIndex)
79{
80 vMissingIndex.clear();
81
82 size_t nTotal = cmpct.vPrefilledIndex.size() + cmpct.vShortTxId.size();
83 if (cmpct.vPrefilledIndex.size() != cmpct.vPrefilledTx.size() || nTotal == 0)
84 return false; // malformed
85
86 // nTotal comes straight off the wire once this is wired into the P2P
87 // layer, and serialize.h only bounds a vector length by MAX_SIZE
88 // (0x02000000), so an unbounded assign() below would let one 60-odd-byte
89 // message ask this node for ~33M default-constructed CTransactions. The
90 // smallest a transaction can serialise to is 10 bytes (nVersion, empty
91 // vin, empty vout, nLockTime), so no block within MAX_BLOCK_SIZE can hold
92 // more than MAX_BLOCK_SIZE/10 of them; anything above that is not a block
93 // we could ever accept. Rejecting here just falls back to full-block
94 // relay, which is always correct.
95 if (nTotal > MAX_BLOCK_SIZE / 10)
96 return false; // implausible tx count -- refuse to size a vector from it
97
98 outBlock.SetNull();
99 outBlock.nVersion = cmpct.nVersion;
100 outBlock.hashPrevBlock = cmpct.hashPrevBlock;
101 outBlock.hashMerkleRoot = cmpct.hashMerkleRoot;
102 outBlock.nTime = cmpct.nTime;
103 outBlock.nBits = cmpct.nBits;
104 outBlock.nNonce = cmpct.nNonce;
105 outBlock.vtx.assign(nTotal, CTransaction());
106
107 std::vector<bool> vFilled(nTotal, false);
108 for (size_t i = 0; i < cmpct.vPrefilledIndex.size(); i++)
109 {
110 unsigned int idx = cmpct.vPrefilledIndex[i];
111 if (idx >= nTotal)
112 return false; // malformed
113 outBlock.vtx[idx] = cmpct.vPrefilledTx[i];
114 vFilled[idx] = true;
115 }
116
117 // Index every mempool tx by its short ID under this block's keying, so
118 // resolving each block position is a map lookup rather than a linear
119 // scan of the mempool per short ID.
120 std::map<uint64, const CTransaction*> mapShortIdToTx;
121 for (std::map<uint256, CTransaction>::const_iterator mi = mapTransactions.begin();
122 mi != mapTransactions.end(); ++mi)
123 {
124 uint64 sid = ComputeShortTxId(cmpct.hashPrevBlock, cmpct.shortIdNonce, mi->first);
125 // A short-ID collision between two distinct mempool transactions is
126 // possible (48 bits, birthday bound ~2^24 mempool entries) though
127 // very unlikely at this chain's scale; on collision, prefer keeping
128 // neither mapped rather than risk resolving to the wrong tx --
129 // that position just falls through to getblocktxn like a genuine
130 // miss, which is always correct, only occasionally not optimal.
131 if (mapShortIdToTx.count(sid))
132 mapShortIdToTx[sid] = NULL;
133 else
134 mapShortIdToTx[sid] = &mi->second;
135 }
136
137 size_t shortIdPos = 0;
138 for (size_t idx = 0; idx < nTotal; idx++)
139 {
140 if (vFilled[idx])
141 continue;
142 if (shortIdPos >= cmpct.vShortTxId.size())
143 return false; // malformed: more unfilled positions than short IDs
144 uint64 sid = cmpct.vShortTxId[shortIdPos++];
145
146 std::map<uint64, const CTransaction*>::const_iterator found = mapShortIdToTx.find(sid);
147 if (found != mapShortIdToTx.end() && found->second != NULL)
148 {
149 outBlock.vtx[idx] = *found->second;
150 vFilled[idx] = true;
151 }
152 else
153 {
154 vMissingIndex.push_back(idx);
155 }
156 }
157
158 return vMissingIndex.empty();
159}
160
161bool FillMissingAndFinish(CBlock& outBlock, const std::vector<unsigned int>& vMissingIndex,
162 const std::vector<CTransaction>& vTxn)
163{
164 if (vMissingIndex.size() != vTxn.size())
165 return false; // peer didn't answer what was asked
166
167 for (size_t i = 0; i < vMissingIndex.size(); i++)
168 {
169 unsigned int idx = vMissingIndex[i];
170 if (idx >= outBlock.vtx.size())
171 return false; // malformed
172 outBlock.vtx[idx] = vTxn[i];
173 }
174 return true;
175}