diff --git a/src/impl/btc/coin/block_assembly.hpp b/src/impl/btc/coin/block_assembly.hpp new file mode 100644 index 000000000..13a14e9fd --- /dev/null +++ b/src/impl/btc/coin/block_assembly.hpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once +// --------------------------------------------------------------------------- +// btc::coin::assemble_won_block -- the share->block "as_block" reassembly that +// the BTC won-block reconstructor (#744 dispatch arc, slice 4/7) feeds to the +// dual-path broadcaster (P2P relay + submitblock RPC fallback). +// +// This is the BTC port of the FRAMING half of p2pool data.py +// Share.as_block(tracker, known_txs): +// +// gentx = self.check(tracker, known_txs) # the coinbase tx +// other_txs = [known_txs[h] for h in transaction_hashes] +// return dict(header=self.header, txs=[gentx]+other_txs) +// +// It mirrors the landed DGB slice (src/impl/dgb/coin/block_assembly.hpp) but is +// deliberately NOT DGB-identical in one consensus-relevant respect -- see +// "BTC divergence" below. +// +// Two consensus-relevant facts this captures (shared with DGB): +// 1. p2pool stores only a SmallBlockHeaderType on the share (version|prev| +// time|bits|nonce -- NO merkle_root). The full block header's merkle_root +// is RECONSTRUCTED as check_merkle_link(gentx_hash, share merkle link), +// exactly as verification recomputed the share's PoW root at check() time +// (share_check.hpp:674-692 / :2500). We MUST reproduce it the same way, or +// the block hashes to something other than the share's PoW-valid header and +// the daemon rejects it. +// 2. Block tx order is [gentx] ++ other_txs, gentx (coinbase) always index 0, +// other_txs in the captured template's transactions[] order (the set the +// share committed to, replayed by template_capture.hpp -- slice 3). +// +// ---- BTC divergence from the DGB slice (the reviewer-load-bearing part) ---- +// BTC commits the BIP141 witness root INSIDE the coinbase at gentx-BUILD time +// (share_check.hpp:2166 / :2296 -- generate_share_transaction). So slices 1-3 +// hand this framer an ALREADY witness-committed gentx. We therefore do NOT +// re-inject a witness commitment here. Re-injecting DGB-style +// add_witness_commitment would MUTATE the coinbase, change its txid, invalidate +// the share-committed header merkle_root, and open a SECOND merkle path -- which +// the sealed BTC merkle family (#570 / #574 / #579) forbids. The DGB lane adds +// its commitment downstream only because it does not commit at gentx-time; BTC +// does, so the correct BTC action is: reuse the committed gentx verbatim. Do NOT +// "fix" this back to the DGB add_witness_commitment shape. +// +// The segwit-vs-legacy merkle-LINK SELECTION (segwit shares use +// segwit_data.txid_merkle_link, legacy shares use merkle_link -- share_check.hpp +// :674-692) stays in the CALLER (the reconstruct closure, slice 5): this framer +// takes an already-resolved MerkleLink, exactly like the DGB slice, so the +// sealed link math is reused with NO new merkle path introduced here. +// +// Body/header self-check (item 3): because BTC hands us the full template tx +// set, we can -- and do -- verify the assembled body against the share-committed +// header BEFORE emitting bytes. body_merkle_root() folds [gentx_hash] ++ the +// non-witness txids of other_txs through the sealed btc::coin::compute_merkle_root +// SSOT (template_builder.hpp); for a body that matches the merkle link the share +// committed to, this equals reconstruct_block_header()'s check_merkle_link root +// by the definition of a coinbase (leaf-0) merkle branch. assemble_won_block +// FAILS CLOSED (throws) on a mismatch rather than emit a bad-txnmrklroot block, +// consistent with the whole slice arc's "never broadcast a malformed block" +// posture (cf. gentx_unpack.hpp). This is a self-check over the EXISTING sealed +// compute_merkle_root, NOT a new merkle path or a second commitment. +// +// Interaction with template_capture.hpp (slice 3): on a capture MISS slice 3 +// replays an EMPTY transactions[] -> other_txs = {}. If the won share mined a +// coinbase-only template (empty merkle link), body_root == header_root == gentx +// txid: the guard passes and the coinbase-only block broadcasts, carrying the +// full subsidy exactly as template_capture documents. If instead the share +// committed to fee txs (non-empty link), a coinbase-only body is a +// bad-txnmrklroot block the daemon would reject anyway; the guard throws so the +// reconstruct closure fails closed (announce + audit) instead of broadcasting a +// doomed block. Nothing broadcastable is lost -- the guard only tightens the +// non-empty-link miss case, which was never a valid block. +// +// Wire encoding is BlockType::Serialize -> TX_WITH_WITNESS(m_txs): the standard +// Bitcoin-Core CONDITIONAL serializer -- it emits the per-tx witness marker/flag +// iff some tx HasWitness(), legacy otherwise. The block's witness shape is thus +// governed by whether the (already-committed) gentx carries a witness, NOT an +// unconditional witness branch here. This is the identical path +// NodeRPC::submit_block uses (rpc.cpp: pack(block) -> submitblock), so +// the reconstructed block round-trips and the daemon accepts. +// +// Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE -- +// block framing reuses BlockType + check_merkle_link + compute_merkle_root +// verbatim; no share format, PoW hash, coinbase commitment, or PPLNS math is +// touched. +// --------------------------------------------------------------------------- + +#include +#include +#include +#include + +#include +#include +#include +#include // HexStr + +#include "block.hpp" // SmallBlockHeaderType, BlockHeaderType, BlockType +#include "transaction.hpp" // MutableTransaction, TX_NO_WITNESS +#include "template_builder.hpp" // btc::coin::compute_merkle_root (sealed body-root SSOT) +#include "../share_check.hpp" // btc::check_merkle_link (SSOT merkle-branch walk) +#include "../share_types.hpp" // btc::MerkleLink + +namespace btc +{ +namespace coin +{ + +// Reconstruct the full block header from the share's stored SmallBlockHeader +// plus the gentx txid + (already segwit-resolved) merkle link. Mirrors the +// merkle_root recomputation in share_check.hpp verification (SmallBlockHeader +// omits merkle_root); the caller passes txid_merkle_link for segwit shares and +// merkle_link for legacy ones (slice 5), so no link selection happens here. +inline BlockHeaderType +reconstruct_block_header(const SmallBlockHeaderType& small_header, + const uint256& gentx_hash, + const ::btc::MerkleLink& merkle_link) +{ + BlockHeaderType header; + header.m_version = small_header.m_version; + header.m_previous_block = small_header.m_previous_block; + header.m_timestamp = small_header.m_timestamp; + header.m_bits = small_header.m_bits; + header.m_nonce = small_header.m_nonce; + // SmallBlockHeader has no merkle_root -- recompute it from the coinbase + // (gentx) txid walked up the share's merkle branch, exactly as the share's + // PoW hash was computed at verification time. + header.m_merkle_root = ::btc::check_merkle_link(gentx_hash, merkle_link); + return header; +} + +// The merkle leaf (txid) of an other-tx: double-SHA256 of its NON-WITNESS +// serialization, identical to the gentx_hash derivation in gentx_unpack.hpp. +// The BTC merkle tree (header root, and the segwit txid_merkle_link) commits to +// TXIDs, never wtxids, so witness data is excluded here regardless of the tx's +// witness shape. +inline uint256 other_tx_txid(const MutableTransaction& tx) +{ + return Hash(pack(TX_NO_WITNESS(tx)).get_span()); +} + +// The body's merkle root: compute_merkle_root over [gentx_hash] ++ other-tx +// txids, using the sealed btc::coin::compute_merkle_root SSOT (#570/#574/#579). +// For a body that matches the merkle link the share committed to, this equals +// reconstruct_block_header()'s check_merkle_link root. +inline uint256 +body_merkle_root(const uint256& gentx_hash, + const std::vector& other_txs) +{ + std::vector txids; + txids.reserve(1 + other_txs.size()); + txids.push_back(gentx_hash); // coinbase is leaf 0 + for (const auto& tx : other_txs) + txids.push_back(other_tx_txid(tx)); + return compute_merkle_root(std::move(txids)); +} + +// Assemble the full serialized parent block for a won share. +// small_header : share.m_min_header (version|prev|time|bits|nonce) +// gentx : the reconstructed, ALREADY witness-committed coinbase (tx 0) +// gentx_hash : its txid (double-SHA256 of the non-witness bytes) +// merkle_link : the share's segwit-resolved gentx->root branch (caller-chosen) +// other_txs : the captured template's non-coinbase txs, in template order +// Returns {block_bytes, block_hex}: block_bytes is the blob the embedded P2P +// relay sends; block_hex is the same block for the external submitblock fallback +// (block_hex == HexStr(block_bytes)). Throws std::runtime_error if the assembled +// body's merkle root does not match the share-committed header root (fail-closed +// against a bad-txnmrklroot block -- see the header comment's template_capture +// interaction note). +inline std::pair, std::string> +assemble_won_block(const SmallBlockHeaderType& small_header, + const MutableTransaction& gentx, + const uint256& gentx_hash, + const ::btc::MerkleLink& merkle_link, + const std::vector& other_txs) +{ + BlockType block; + static_cast(block) = + reconstruct_block_header(small_header, gentx_hash, merkle_link); + + // Fail-closed body/header consistency guard: the share-committed header root + // (the merkle_link walk) MUST equal the root the assembled body actually + // hashes to, or the daemon rejects the block as bad-txnmrklroot. Reuses the + // sealed compute_merkle_root SSOT -- NOT a new merkle path or a re-inject. + const uint256 body_root = body_merkle_root(gentx_hash, other_txs); + if (body_root != block.m_merkle_root) + throw std::runtime_error( + "assemble_won_block: assembled body merkle root " + body_root.GetHex() + + " != share-committed header root " + block.m_merkle_root.GetHex() + + " -- refusing to broadcast a bad-txnmrklroot block"); + + // txs = [gentx] ++ other_txs (coinbase first; data.py as_block ordering). + block.m_txs.reserve(1 + other_txs.size()); + block.m_txs.push_back(gentx); + for (const auto& tx : other_txs) + block.m_txs.push_back(tx); + + PackStream packed = pack(block); + auto sp = packed.get_span(); + std::vector bytes( + reinterpret_cast(sp.data()), + reinterpret_cast(sp.data()) + sp.size()); + std::string hex = HexStr(sp); + return {std::move(bytes), std::move(hex)}; +} + +} // namespace coin +} // namespace btc diff --git a/src/impl/btc/test/CMakeLists.txt b/src/impl/btc/test/CMakeLists.txt index 1ac6eb6d1..c113eee95 100644 --- a/src/impl/btc/test/CMakeLists.txt +++ b/src/impl/btc/test/CMakeLists.txt @@ -1,7 +1,7 @@ if (BUILD_TESTING AND GTest_FOUND) # btc twin of ltc share_test — uniquely named to avoid the CMP0002 target # collision with src/impl/ltc/test (both subdirs build in the same tree). - add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp won_share_dualpath_test.cpp gentx_unpack_test.cpp gentx_coinbase_test.cpp) + add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp won_share_dualpath_test.cpp gentx_unpack_test.cpp block_assembly_test.cpp gentx_coinbase_test.cpp) target_link_libraries(btc_share_test PRIVATE GTest::gtest_main GTest::gtest core btc diff --git a/src/impl/btc/test/block_assembly_test.cpp b/src/impl/btc/test/block_assembly_test.cpp new file mode 100644 index 000000000..3994b6c2a --- /dev/null +++ b/src/impl/btc/test/block_assembly_test.cpp @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// --------------------------------------------------------------------------- +// btc::coin::assemble_won_block / reconstruct_block_header / body_merkle_root +// test (#744 broadcaster arc, faithful as_block FRAMING half -- slice 4/7). +// +// Locks the share->block reassembly contract the BTC won-block reconstructor +// feeds to the dual-path broadcaster, mirroring p2pool data.py Share.as_block: +// * the full header's merkle_root is RECONSTRUCTED from the gentx txid walked +// up the share's (segwit-resolved) merkle link -- SmallBlockHeader stores no +// merkle_root -- empty branch => root == gentx_hash; index bit selects side; +// * block txs are [gentx] ++ other_txs, coinbase at index 0, template order; +// * the assembled body's merkle root (sealed compute_merkle_root over the +// non-witness txids) MUST match the share-committed header root, else +// assemble_won_block FAILS CLOSED (throws) rather than emit a +// bad-txnmrklroot block; +// * the block round-trips through BlockType (the live submitblock wire path), +// block_hex == HexStr(block_bytes), and the CONDITIONAL witness codec emits +// a witness block iff the (already-committed) gentx carries a witness. +// +// The gentx enters as an already witness-committed MutableTransaction + its +// non-witness txid (BTC commits at gentx-build time -- share_check.hpp:2166/ +// :2296 -- so this framer does NOT re-inject a witness commitment). KATs are +// self-derived via the same Hash() the merkle walk uses, independent of any +// fixture file, and build every non-empty merkle link from the SSOT +// other_tx_txid so the body/header guard is exercised on genuinely consistent +// inputs. +// +// MUST appear in src/impl/btc/test/CMakeLists.txt's btc_share_test target (it +// does) or it becomes a NOT_BUILT sentinel; btc_share_test is already on the +// build.yml --target allowlist, so no workflow change is needed. +// --------------------------------------------------------------------------- + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace { + +using btc::coin::BlockType; +using btc::coin::SmallBlockHeaderType; +using btc::coin::MutableTransaction; +using btc::coin::TxIn; +using btc::coin::TxOut; +using btc::coin::assemble_won_block; +using btc::coin::reconstruct_block_header; +using btc::coin::body_merkle_root; +using btc::coin::other_tx_txid; + +// A minimal coinbase-shaped gentx: one input spending the null outpoint, one +// output. Exact bytes are irrelevant to the framing math -- it just needs to +// serialize and round-trip. +MutableTransaction make_gentx() +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0xffffffff; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + TxOut out; + out.value = 5000000000LL; + tx.vout.push_back(out); + return tx; +} + +MutableTransaction make_tx(int64_t value) +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + TxOut out; + out.value = value; + tx.vout.push_back(out); + return tx; +} + +SmallBlockHeaderType make_small_header() +{ + SmallBlockHeaderType h; + h.m_version = 0x20000000; // a non-trivial version (algo bits live here) + h.m_previous_block.SetHex("00000000000000000000000000000000000000000000000000000000deadbeef"); + h.m_timestamp = 1718700000; + h.m_bits = 0x1a0fffff; + h.m_nonce = 0x12345678; + return h; +} + +// Replicate one merkle-branch combine step the way btc::check_merkle_link does: +// if the index bit is set, branch is on the LEFT, else cur is on the LEFT; then +// double-SHA256 the 64-byte concat. combine(a,b,false) also equals a single +// compute_merkle_root pair fold of [a,b]. +uint256 combine(const uint256& cur, const uint256& branch, bool branch_left) +{ + PackStream ps; + if (branch_left) { ps << branch; ps << cur; } + else { ps << cur; ps << branch; } + auto sp = std::span( + reinterpret_cast(ps.data()), ps.size()); + return Hash(sp); +} +uint256 pair_hash(const uint256& a, const uint256& b) { return combine(a, b, /*branch_left=*/false); } + +uint256 fixed_gentx_hash() +{ + uint256 h; + h.SetHex("1111111111111111111111111111111111111111111111111111111111111111"); + return h; +} + +// --- Test 1: empty merkle branch => merkle_root == gentx_hash, header copied -- +TEST(BtcBlockAssembly, EmptyBranchRootIsGentxHash) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::btc::MerkleLink link; // empty branch, index 0 + + auto header = reconstruct_block_header(sh, gtx_hash, link); + + EXPECT_EQ(header.m_merkle_root, gtx_hash); + EXPECT_EQ(header.m_version, sh.m_version); + EXPECT_EQ(header.m_previous_block, sh.m_previous_block); + EXPECT_EQ(header.m_timestamp, sh.m_timestamp); + EXPECT_EQ(header.m_bits, sh.m_bits); + EXPECT_EQ(header.m_nonce, sh.m_nonce); +} + +// --- Test 2: one-branch link, index bit 0 => cur on left (KAT) ---------------- +TEST(BtcBlockAssembly, SingleBranchIndexZero) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::btc::MerkleLink link; + uint256 b0; b0.SetHex("2222222222222222222222222222222222222222222222222222222222222222"); + link.m_branch.push_back(b0); + link.m_index = 0; + + auto header = reconstruct_block_header(sh, gtx_hash, link); + EXPECT_EQ(header.m_merkle_root, combine(gtx_hash, b0, /*branch_left=*/false)); +} + +// --- Test 3: index bit set => branch on left (order matters) ------------------- +TEST(BtcBlockAssembly, SingleBranchIndexOne) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::btc::MerkleLink link; + uint256 b0; b0.SetHex("2222222222222222222222222222222222222222222222222222222222222222"); + link.m_branch.push_back(b0); + link.m_index = 1; + + auto header = reconstruct_block_header(sh, gtx_hash, link); + EXPECT_EQ(header.m_merkle_root, combine(gtx_hash, b0, /*branch_left=*/true)); + // index discriminates side: the two orderings must differ. + EXPECT_NE(combine(gtx_hash, b0, true), combine(gtx_hash, b0, false)); +} + +// --- Test 4: body_merkle_root is the sealed compute_merkle_root SSOT ---------- +TEST(BtcBlockAssembly, BodyMerkleRootMatchesSSOT) +{ + auto gtx_hash = fixed_gentx_hash(); + // no other txs => root == gentx_hash (single leaf). + EXPECT_EQ(body_merkle_root(gtx_hash, {}), gtx_hash); + + // one other tx => pair fold of [gentx_hash, txid(t1)]. + auto t1 = make_tx(10); + auto t1id = other_tx_txid(t1); + EXPECT_EQ(body_merkle_root(gtx_hash, {t1}), pair_hash(gtx_hash, t1id)); + // cross-check against the sealed compute_merkle_root directly. + EXPECT_EQ(body_merkle_root(gtx_hash, {t1}), + btc::coin::compute_merkle_root({gtx_hash, t1id})); +} + +// --- Test 5: full assemble round-trips, coinbase first, hex==HexStr(bytes) ----- +// Two-leaf tree: leaves [gentx, t1]; coinbase (leaf 0) branch = [txid(t1)]. +TEST(BtcBlockAssembly, AssembleRoundTripsCoinbaseFirst) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gtx_hash = fixed_gentx_hash(); + auto tx1 = make_tx(10); + auto t1id = other_tx_txid(tx1); + + ::btc::MerkleLink link; // consistent leaf-0 branch for [gentx, t1] + link.m_branch.push_back(t1id); + link.m_index = 0; + std::vector other = { tx1 }; + + auto [bytes, hex] = assemble_won_block(sh, gentx, gtx_hash, link, other); + + ASSERT_FALSE(bytes.empty()); + auto sp = std::span( + reinterpret_cast(bytes.data()), bytes.size()); + EXPECT_EQ(hex, HexStr(sp)); + + // Round-trip through the live BlockType wire codec. + PackStream ps(bytes); + BlockType blk; + ps >> blk; + + EXPECT_EQ(blk.m_version, sh.m_version); + EXPECT_EQ(blk.m_previous_block, sh.m_previous_block); + EXPECT_EQ(blk.m_timestamp, sh.m_timestamp); + EXPECT_EQ(blk.m_bits, sh.m_bits); + EXPECT_EQ(blk.m_nonce, sh.m_nonce); + EXPECT_EQ(blk.m_merkle_root, pair_hash(gtx_hash, t1id)); + + // txs = [gentx] ++ other_txs : coinbase at index 0, total = 1 + 1. + ASSERT_EQ(blk.m_txs.size(), 2u); + // tx 0 is the coinbase (gentx): spends the null outpoint at 0xffffffff. + EXPECT_EQ(blk.m_txs[0].version, gentx.version); + ASSERT_EQ(blk.m_txs[0].vin.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); + EXPECT_TRUE(blk.m_txs[0].vin[0].prevout.hash.IsNull()); + ASSERT_EQ(blk.m_txs[0].vout.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vout[0].value, gentx.vout[0].value); + // the other_tx follows. + ASSERT_EQ(blk.m_txs[1].vout.size(), 1u); + EXPECT_EQ(blk.m_txs[1].vout[0].value, 10); +} + +// --- Test 6: multi-tx ordering preserved (3-leaf consistent tree) -------------- +// leaves [gentx, t1, t2]; compute_merkle_root duplicates the odd last leaf, so +// the coinbase branch is [txid(t1), pair(txid(t2), txid(t2))]. +TEST(BtcBlockAssembly, MultiTxOrderPreserved) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gtx_hash = fixed_gentx_hash(); + auto tx1 = make_tx(11); + auto tx2 = make_tx(22); + auto t1id = other_tx_txid(tx1); + auto t2id = other_tx_txid(tx2); + + ::btc::MerkleLink link; + link.m_branch.push_back(t1id); + link.m_branch.push_back(pair_hash(t2id, t2id)); // odd-leaf duplication + link.m_index = 0; + std::vector other = { tx1, tx2 }; + + // sanity: the branch we built reconstructs the same root the body hashes to. + ASSERT_EQ(reconstruct_block_header(sh, gtx_hash, link).m_merkle_root, + body_merkle_root(gtx_hash, other)); + + auto [bytes, hex] = assemble_won_block(sh, gentx, gtx_hash, link, other); + PackStream ps(bytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 3u); + EXPECT_EQ(blk.m_txs[1].vout[0].value, 11); // t1 before t2 + EXPECT_EQ(blk.m_txs[2].vout[0].value, 22); +} + +// --- Test 7: no other_txs => single-tx block (coinbase only) ------------------- +TEST(BtcBlockAssembly, CoinbaseOnlyBlock) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gtx_hash = fixed_gentx_hash(); + ::btc::MerkleLink link; // empty branch => root == gtx_hash, body matches + std::vector none; + + auto [bytes, hex] = assemble_won_block(sh, gentx, gtx_hash, link, none); + + PackStream ps(bytes); + BlockType blk; + ps >> blk; + EXPECT_EQ(blk.m_txs.size(), 1u); + EXPECT_EQ(blk.m_merkle_root, gtx_hash); +} + +// --- Test 8: body/header mismatch FAILS CLOSED (never emit bad-txnmrklroot) ---- +TEST(BtcBlockAssembly, MismatchedBodyThrows) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gtx_hash = fixed_gentx_hash(); + // Header commits to a coinbase-only root (empty branch) but the body carries + // a fee tx: the assembled body root (pair fold) != the header root -- exactly + // the template-capture non-empty-link miss case. Must throw, not emit. + ::btc::MerkleLink empty_link; + std::vector other = { make_tx(99) }; + EXPECT_THROW(assemble_won_block(sh, gentx, gtx_hash, empty_link, other), + std::runtime_error); + + // Conversely, a wrong (non-matching) branch with a matching-count body also + // fails: branch sibling that is not txid(t1). + auto tx1 = make_tx(99); + ::btc::MerkleLink bad_link; + uint256 wrong; wrong.SetHex("dead00000000000000000000000000000000000000000000000000000000beef"); + bad_link.m_branch.push_back(wrong); + bad_link.m_index = 0; + EXPECT_THROW(assemble_won_block(sh, gentx, gtx_hash, bad_link, { tx1 }), + std::runtime_error); +} + +// === Witness predicate KATs (BTC gentx-time commitment) ======================= +// The block wire codec is BlockType::Serialize -> TX_WITH_WITNESS(m_txs), the +// standard Bitcoin-Core CONDITIONAL serializer: it emits the per-tx witness +// marker/flag (and stacks) iff some tx HasWitness(), a legacy blob otherwise. So +// the won-block's witness shape is governed by whether the ALREADY-COMMITTED +// gentx carries a witness -- NOT by an unconditional witness branch in the +// framer, and NOT by any re-injection here. These pin emission == verification. + +// The BIP141 coinbase witness reserved value: a single 32-byte zero stack item. +MutableTransaction make_segwit_gentx() +{ + auto tx = make_gentx(); + tx.vin[0].scriptWitness.stack.assign(1, std::vector(32, 0x00)); + return tx; +} + +// --- Test 9: segwit gentx => TX_WITH_WITNESS block (predicate true) ------------ +// Coinbase-only (empty branch) so the body/header guard passes on the reserved +// gentx_hash; the point under test is the conditional witness emission. +TEST(BtcBlockAssembly, SegwitGentxEmitsWitnessBlock) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::btc::MerkleLink link; + std::vector none; + + auto seg = make_segwit_gentx(); + ASSERT_TRUE(seg.HasWitness()); // gentx carries the coinbase witness + auto [wbytes, whex] = assemble_won_block(sh, seg, gtx_hash, link, none); + + // The conditional codec must have emitted the witness: the witnessful block + // is strictly larger than the legacy block over the same logical txs, and + // the round-tripped coinbase preserves the reserved value. + auto [lbytes, lhex] = assemble_won_block(sh, make_gentx(), gtx_hash, link, none); + EXPECT_GT(wbytes.size(), lbytes.size()); + + PackStream ps(wbytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 1u); + EXPECT_TRUE(blk.m_txs[0].HasWitness()); + ASSERT_EQ(blk.m_txs[0].vin.size(), 1u); + ASSERT_EQ(blk.m_txs[0].vin[0].scriptWitness.stack.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vin[0].scriptWitness.stack[0], + std::vector(32, 0x00)); + EXPECT_EQ(whex, HexStr(std::span( + reinterpret_cast(wbytes.data()), wbytes.size()))); +} + +// --- Test 10: no-witness gentx => LEGACY block (predicate false) --------------- +TEST(BtcBlockAssembly, LegacyGentxEmitsLegacyBlock) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::btc::MerkleLink link; + std::vector none; + + auto plain = make_gentx(); + ASSERT_FALSE(plain.HasWitness()); + auto [bytes, hex] = assemble_won_block(sh, plain, gtx_hash, link, none); + + PackStream ps(bytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 1u); + EXPECT_FALSE(blk.m_txs[0].HasWitness()); // legacy: no marker/flag emitted + + // Legacy block re-serializes byte-identically (no witness round-trip drift). + auto [bytes2, hex2] = assemble_won_block(sh, blk.m_txs[0], gtx_hash, link, none); + EXPECT_EQ(hex, hex2); +} + +} // namespace