diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
| commit | ceae3d076b5ad11ee84729ae43f14e9312896646 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/compactblock.cpp | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | 66e0b9158d050c46b1ff2509faeee3f4573cac1c (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.cpp | 175 |
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 | |||
| 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 | // 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 | |||
| 78 | bool 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 | |||
| 161 | bool 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 | } | ||