// Copyright (c) 2026 Bitflash developers // Distributed under the MIT/X11 software license. // // See compactblock.h. #include "headers.h" #include "compactblock.h" #include #include uint256 CCmpctBlock::HeaderHash() const { // Same technique CBlock::GetHash() uses: the header fields are laid out // contiguously and in the same order here as in CBlock, so hashing the // raw member range gives the identical block-identity hash. return Hash(BEGIN(nVersion), END(nNonce)); } uint64 ComputeShortTxId(const uint256& hashPrevBlock, uint64 shortIdNonce, const uint256& txid) { // Derive a 16-byte SipHash key from (hashPrevBlock, shortIdNonce), so // short IDs are specific to this block: they can't be precomputed // before the block (and its freshly-chosen nonce) exist, and the same // transaction gets a different short ID in every block it appears in // (relevant across reorgs/re-announcements). unsigned char keyMaterial[32 + sizeof(uint64)]; memcpy(keyMaterial, BEGIN(hashPrevBlock), 32); memcpy(keyMaterial + 32, &shortIdNonce, sizeof(uint64)); uint256 keyHash = Hash((const char*)keyMaterial, (const char*)keyMaterial + sizeof(keyMaterial)); unsigned char out[crypto_shorthash_BYTES]; // 8 bytes crypto_shorthash(out, (const unsigned char*)BEGIN(txid), 32, (const unsigned char*)BEGIN(keyHash)); uint64 id = 0; memcpy(&id, out, sizeof(uint64)); return id & 0xFFFFFFFFFFFFULL; // low 48 bits -- matches the wire field width } bool BuildCompactBlock(const CBlock& block, CCmpctBlock& out) { if (block.vtx.empty()) return false; // every real block has at least the coinbase out.nVersion = block.nVersion; out.hashPrevBlock = block.hashPrevBlock; out.hashMerkleRoot = block.hashMerkleRoot; out.nTime = block.nTime; out.nBits = block.nBits; out.nNonce = block.nNonce; // Upstream ignored the RAND_bytes return value here. On failure nonceBytes // is left uninitialised and an indeterminate shortIdNonce goes out on the // wire -- which is precisely the value this file relies on being // unpredictable to stop short IDs being precomputed or ground against. // Fail closed instead, matching every other RAND_bytes call site in this // tree (bip32.cpp, db.cpp): the caller falls back to full-block relay, // which is always correct. unsigned char nonceBytes[sizeof(uint64)]; if (RAND_bytes(nonceBytes, sizeof(nonceBytes)) != 1) return false; memcpy(&out.shortIdNonce, nonceBytes, sizeof(uint64)); out.vPrefilledIndex.clear(); out.vPrefilledTx.clear(); out.vShortTxId.clear(); // Only the coinbase is prefilled (index 0) -- see file comment in // compactblock.h for why that's enough for this chain's scale. out.vPrefilledIndex.push_back(0); out.vPrefilledTx.push_back(block.vtx[0]); for (size_t i = 1; i < block.vtx.size(); i++) out.vShortTxId.push_back(ComputeShortTxId(out.hashPrevBlock, out.shortIdNonce, block.vtx[i].GetHash())); return true; } bool TryReconstructBlock(const CCmpctBlock& cmpct, CBlock& outBlock, std::vector& vMissingIndex) { vMissingIndex.clear(); size_t nTotal = cmpct.vPrefilledIndex.size() + cmpct.vShortTxId.size(); if (cmpct.vPrefilledIndex.size() != cmpct.vPrefilledTx.size() || nTotal == 0) return false; // malformed // nTotal comes straight off the wire once this is wired into the P2P // layer, and serialize.h only bounds a vector length by MAX_SIZE // (0x02000000), so an unbounded assign() below would let one 60-odd-byte // message ask this node for ~33M default-constructed CTransactions. The // smallest a transaction can serialise to is 10 bytes (nVersion, empty // vin, empty vout, nLockTime), so no block within MAX_BLOCK_SIZE can hold // more than MAX_BLOCK_SIZE/10 of them; anything above that is not a block // we could ever accept. Rejecting here just falls back to full-block // relay, which is always correct. if (nTotal > MAX_BLOCK_SIZE / 10) return false; // implausible tx count -- refuse to size a vector from it outBlock.SetNull(); outBlock.nVersion = cmpct.nVersion; outBlock.hashPrevBlock = cmpct.hashPrevBlock; outBlock.hashMerkleRoot = cmpct.hashMerkleRoot; outBlock.nTime = cmpct.nTime; outBlock.nBits = cmpct.nBits; outBlock.nNonce = cmpct.nNonce; outBlock.vtx.assign(nTotal, CTransaction()); std::vector vFilled(nTotal, false); for (size_t i = 0; i < cmpct.vPrefilledIndex.size(); i++) { unsigned int idx = cmpct.vPrefilledIndex[i]; if (idx >= nTotal) return false; // malformed outBlock.vtx[idx] = cmpct.vPrefilledTx[i]; vFilled[idx] = true; } // Index every mempool tx by its short ID under this block's keying, so // resolving each block position is a map lookup rather than a linear // scan of the mempool per short ID. std::map mapShortIdToTx; for (std::map::const_iterator mi = mapTransactions.begin(); mi != mapTransactions.end(); ++mi) { uint64 sid = ComputeShortTxId(cmpct.hashPrevBlock, cmpct.shortIdNonce, mi->first); // A short-ID collision between two distinct mempool transactions is // possible (48 bits, birthday bound ~2^24 mempool entries) though // very unlikely at this chain's scale; on collision, prefer keeping // neither mapped rather than risk resolving to the wrong tx -- // that position just falls through to getblocktxn like a genuine // miss, which is always correct, only occasionally not optimal. if (mapShortIdToTx.count(sid)) mapShortIdToTx[sid] = NULL; else mapShortIdToTx[sid] = &mi->second; } size_t shortIdPos = 0; for (size_t idx = 0; idx < nTotal; idx++) { if (vFilled[idx]) continue; if (shortIdPos >= cmpct.vShortTxId.size()) return false; // malformed: more unfilled positions than short IDs uint64 sid = cmpct.vShortTxId[shortIdPos++]; std::map::const_iterator found = mapShortIdToTx.find(sid); if (found != mapShortIdToTx.end() && found->second != NULL) { outBlock.vtx[idx] = *found->second; vFilled[idx] = true; } else { vMissingIndex.push_back(idx); } } return vMissingIndex.empty(); } bool FillMissingAndFinish(CBlock& outBlock, const std::vector& vMissingIndex, const std::vector& vTxn) { if (vMissingIndex.size() != vTxn.size()) return false; // peer didn't answer what was asked for (size_t i = 0; i < vMissingIndex.size(); i++) { unsigned int idx = vMissingIndex[i]; if (idx >= outBlock.vtx.size()) return false; // malformed outBlock.vtx[idx] = vTxn[i]; } return true; }