From d1ddef194466c7e16594d328c3b7d2efe04700db Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sat, 25 Jul 2026 01:20:24 +0000 Subject: [PATCH] =?UTF-8?q?btc(coin):=20faithful=20won-block=20reconstruct?= =?UTF-8?q?or=20closure=20=E2=80=94=20reconstruct=5Fwon=5Fblock=20(reconst?= =?UTF-8?q?ructor=20slice=205/7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose the landed sub-slices into the run-loop WonBlockReconstructor the mirroring p2pool data.py Share.as_block: * select_won_block_merkle_link — segwit link-selection SSOT (share_check.hpp:674-692): segwit-activated shares with segwit_data walk segwit_data.txid_merkle_link, legacy shares walk merkle_link. The framer (block_assembly.hpp, slice 4) takes an already-resolved MerkleLink, so the selection lives here in the caller — no new merkle path downstream. * reconstruct_won_block — select link + assemble_won_block, taking the captured GBT template (slice 3) as the non-coinbase tx source (NOT the share tx_hash_refs); version-agnostic, coinbase-only on a capture miss. * make_reconstruct_closure — the fail-closed run-loop reconstructor: any unknown share / malformed gentx / body-header merkle mismatch is caught, logged loudly, and yields std::nullopt (never throws out of the compute thread, never broadcasts a partial/wrong block; submitblock RPC fallback still attempts independently). Seam-first: share-field lookup, gentx regen, and the template-tx snapshot are injected callables, so the whole closure is unit-testable with no live ShareTracker / TemplateCapture. main_btc run-loop binding is slice 7. reconstruct_test.cpp: 12 KATs — link selection (segwit/legacy/no-segwit-data), body round-trip through the live BlockType codec, segwit-link-is-walked, mismatch throw, closure fail-closed on malformed gentx + merkle mismatch, and end-to-end wiring into make_on_block_found (both dual-path arms carry the byte-identical block). btc_share_test 94/94 green; diff btc-tree-only. Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE. --- src/impl/btc/coin/reconstruct_won_block.hpp | 219 ++++++++++ src/impl/btc/test/CMakeLists.txt | 2 +- src/impl/btc/test/reconstruct_test.cpp | 426 ++++++++++++++++++++ 3 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 src/impl/btc/coin/reconstruct_won_block.hpp create mode 100644 src/impl/btc/test/reconstruct_test.cpp diff --git a/src/impl/btc/coin/reconstruct_won_block.hpp b/src/impl/btc/coin/reconstruct_won_block.hpp new file mode 100644 index 000000000..279f311cf --- /dev/null +++ b/src/impl/btc/coin/reconstruct_won_block.hpp @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once +// --------------------------------------------------------------------------- +// btc::coin::reconstruct_won_block / make_reconstruct_closure -- the won-block +// reconstructor's (#744 dispatch arc, slice 5/7) BODY + run-loop closure: the +// single composition the dispatch handler (won_block_dispatch.hpp, +// make_on_block_found) injects as its WonBlockReconstructor. It ties the landed +// sub-slices into one C++ implementation of the 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) +// +// Composition (each step is its own KAT'd SSOT slice): +// 1. select_won_block_merkle_link (this file) +// share_version + merkle_link + optional txid_merkle_link --segwit gate--> +// the merkle link the framer must walk gentx_hash up to reproduce the +// share-committed header root (share_check.hpp:674-692). SEGWIT LINK +// SELECTION LIVES HERE (the caller), not in the framer -- block_assembly.hpp +// takes an already-resolved MerkleLink so the sealed link math is reused +// with NO new merkle path introduced downstream. +// 2. unpack_gentx_coinbase (gentx_unpack.hpp, slice 1 / #822) +// the share's SSOT non-witness gentx bytes --> {MutableTransaction, txid} +// injected at block tx index 0. +// 3. assemble_won_block (block_assembly.hpp, as_block FRAMING / #838) +// small_header + gentx + resolved merkle_link + other_txs --> {bytes, hex} +// (merkle_root recomputed from gentx_hash up the link; txs = [gentx] ++ +// other_txs; body/header consistency guard fails closed; BlockType codec +// is the live submitblock path). +// +// ---- BTC won-block tx source: the CAPTURED GBT TEMPLATE, not tx_hash_refs ---- +// The broadcast block's non-coinbase tx set is the GBT template snapshot the +// miner was handed at job hand-out time (template_capture.hpp, slice 3), NOT the +// share's transaction_hash_refs. The ref-walk (DGB other_tx_resolver) is a +// share-CHAIN peer-propagation mechanism; it is never the block-broadcast +// source. That is why this path is version-AGNOSTIC: pre-v34 vs v34+/v36 makes +// no difference, because the share never carried the block tx set in the first +// place. The template txs are INJECTED as a callable (template_other_txs_fn), so +// this whole reconstruction is unit-testable against a synthetic template set +// with NO live TemplateCapture / mempool. On a capture MISS slice 3 replays an +// EMPTY transactions[] -> other_txs = {} -> a valid coinbase-only block (the +// full subsidy), which the body/header guard accepts iff the won share committed +// to an empty merkle link (see block_assembly.hpp's template_capture note). +// +// FAIL-CLOSED (integrator 2026-06-19/20, REWARD-PATH CRITICAL): make_on_block_found +// fires this callback from the compute thread on a won share. It NEVER throws out +// of the callback and NEVER emits a partial/wrong block: ANY missing share, +// gentx-unpack failure, or body/header merkle mismatch is caught, logged LOUDLY, +// and yields std::nullopt -- the share is announced + audited, the submitblock +// RPC fallback still attempts independently (block_broadcast.hpp), and no +// malformed block reaches the network. A wrong reconstruction would hash to the +// wrong merkle root and be daemon-rejected anyway, so failing closed here is +// strictly safer than emitting it. +// +// SEAM-FIRST (mirrors every #744 sub-slice): the share-field lookup, the gentx +// regeneration, and the template-tx snapshot are INJECTED as callables, so the +// whole closure -- including its fail-closed posture -- is unit-testable against +// a synthetic share set with NO live ShareTracker / TemplateCapture. In the +// run-loop (main_btc, slice 7) they bind to: +// share_fields_fn = chain.get_share(h) -> { m_min_header, VERSION, +// m_merkle_link, m_segwit_data->m_txid_merkle_link } +// gentx_bytes_fn = generate_share_transaction(share, ...).bytes (SSOT) +// -> unpack_gentx_coinbase -> { tx, txid } +// template_other_txs_fn = TemplateCapture replay for the won share's job +// (slice 3), in template order +// +// Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE -- +// pure composition of already-validated share_info + the captured template + the +// proven BlockType serializer; no share format, PoW, coinbase commitment, or +// PPLNS math is touched. +// --------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "block_assembly.hpp" // assemble_won_block, SmallBlockHeaderType, MutableTransaction +#include "gentx_unpack.hpp" // unpack_gentx_coinbase, UnpackedGentx +#include "won_block_dispatch.hpp" // WonBlockReconstructor +#include "../share_types.hpp" // btc::MerkleLink, btc::is_segwit_activated + +namespace btc +{ +namespace coin +{ + +// The reconstructed parent block, ready for broadcast_block_for_connect's dual +// path: +// bytes : the pre-serialized blob the embedded P2P relay sends +// hex : the same block for the external submitblock (RPC) fallback +struct ReconstructedWonBlock +{ + std::vector bytes; + std::string hex; +}; + +// The per-share inputs the closure resolves before framing -- everything that is +// NOT the gentx or the template tx set. +// small_header : share.m_min_header (version|prev|time|bits|nonce) +// share_version : the share format VERSION (segwit-activation gate input) +// merkle_link : share.m_merkle_link (legacy gentx->root branch) +// txid_merkle_link : share.m_segwit_data->m_txid_merkle_link if the share +// carried segwit_data, else std::nullopt +struct WonShareReconstructFields +{ + SmallBlockHeaderType small_header; + uint64_t share_version{0}; + ::btc::MerkleLink merkle_link; + std::optional<::btc::MerkleLink> txid_merkle_link; +}; + +// Segwit merkle-link SELECTION SSOT -- mirrors share_check.hpp:674-692 verbatim +// at runtime. A segwit-activated share that carried segwit_data committed its +// header merkle_root over segwit_data.txid_merkle_link; a legacy share (or a +// segwit-active share with no segwit_data sentinel) committed over merkle_link. +// The framer must walk gentx_hash up the SAME link the share committed to, or +// the reconstructed header hashes to a different root than the share's PoW-valid +// header and the daemon rejects the block. Returns a reference into one of the +// two arguments (no copy). +inline const ::btc::MerkleLink& +select_won_block_merkle_link(uint64_t share_version, + const ::btc::MerkleLink& merkle_link, + const std::optional<::btc::MerkleLink>& txid_merkle_link) +{ + if (::btc::is_segwit_activated(share_version) && txid_merkle_link.has_value()) + return txid_merkle_link.value(); + return merkle_link; +} + +// Reconstruct the full serialized parent block for a won share. +// small_header : share.m_min_header (version|prev|time|bits|nonce) +// share_version : the share format VERSION (segwit-activation gate) +// merkle_link : share.m_merkle_link (legacy path) +// txid_merkle_link : share.m_segwit_data->m_txid_merkle_link, or nullopt +// gentx : the share's ALREADY witness-committed coinbase (tx 0) +// gentx_hash : its txid (double-SHA256 of the non-witness bytes) +// template_other_txs : the captured template's non-coinbase txs, template order +// Returns {bytes, hex} with hex == HexStr(bytes). Throws (via assemble_won_block) +// if the assembled body's merkle root does not match the share-committed header +// root -- fail-closed against a bad-txnmrklroot block. +inline ReconstructedWonBlock +reconstruct_won_block(const SmallBlockHeaderType& small_header, + uint64_t share_version, + const ::btc::MerkleLink& merkle_link, + const std::optional<::btc::MerkleLink>& txid_merkle_link, + const MutableTransaction& gentx, + const uint256& gentx_hash, + const std::vector& template_other_txs) +{ + // 1. segwit-resolve the link the share committed its header root to. + const ::btc::MerkleLink& link = + select_won_block_merkle_link(share_version, merkle_link, txid_merkle_link); + + // 2. frame [gentx] ++ template_other_txs via the assemble_won_block SSOT + // (recompute header root up `link`; body/header guard; BlockType codec). + auto framed = assemble_won_block(small_header, gentx, gentx_hash, link, + template_other_txs); + return ReconstructedWonBlock{std::move(framed.first), std::move(framed.second)}; +} + +// Build the run-loop WonBlockReconstructor the dispatch handler +// (make_on_block_found) installs. See the header note for the run-loop bindings +// of each injected seam. The returned callable is FAIL-CLOSED: it returns +// std::nullopt (never throws, never a partial block) on ANY error -- an unknown +// share, a malformed gentx serialization, or a body/header merkle mismatch. +inline WonBlockReconstructor +make_reconstruct_closure( + std::function share_fields_fn, + std::function(const uint256&)> gentx_bytes_fn, + std::function(const uint256&)> template_other_txs_fn) +{ + return [share_fields_fn = std::move(share_fields_fn), + gentx_bytes_fn = std::move(gentx_bytes_fn), + template_other_txs_fn = std::move(template_other_txs_fn)]( + const uint256& share_hash) + -> std::optional, std::string>> + { + try + { + // 1. share-level fields (header / version / merkle links). + const WonShareReconstructFields f = share_fields_fn(share_hash); + + // 2. regenerate + unpack the share's SSOT gentx (block tx index 0); + // unpack throws on a malformed (trailing-byte) serialization. + const UnpackedGentx ug = unpack_gentx_coinbase(gentx_bytes_fn(share_hash)); + + // 3. the captured-GBT template's non-coinbase set (template order; + // empty on a capture miss => a valid coinbase-only block). + const std::vector other_txs = + template_other_txs_fn(share_hash); + + // 4. select segwit link + frame the block (guard fails closed). + ReconstructedWonBlock r = reconstruct_won_block( + f.small_header, f.share_version, f.merkle_link, f.txid_merkle_link, + ug.tx, ug.txid, other_txs); + + return std::make_pair(std::move(r.bytes), std::move(r.hex)); + } + catch (const std::exception& e) + { + // Fail closed: announce + audit, never broadcast a partial/wrong + // block. The submitblock RPC fallback still attempts independently. + LOG_WARNING << "[EMB-BTC] won share " << share_hash.GetHex().substr(0, 16) + << " -- reconstruct FAILED CLOSED (" << e.what() + << "); NOT broadcast on P2P arm (submitblock RPC fallback still attempts)."; + return std::nullopt; + } + }; +} + +} // namespace coin +} // namespace btc diff --git a/src/impl/btc/test/CMakeLists.txt b/src/impl/btc/test/CMakeLists.txt index aa0358a6c..18b2e4310 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 block_assembly_test.cpp gentx_coinbase_test.cpp template_capture_test.cpp template_other_txs_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 template_capture_test.cpp template_other_txs_test.cpp reconstruct_test.cpp) target_link_libraries(btc_share_test PRIVATE GTest::gtest_main GTest::gtest core btc diff --git a/src/impl/btc/test/reconstruct_test.cpp b/src/impl/btc/test/reconstruct_test.cpp new file mode 100644 index 000000000..7059422ac --- /dev/null +++ b/src/impl/btc/test/reconstruct_test.cpp @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// --------------------------------------------------------------------------- +// btc::coin::reconstruct_won_block / select_won_block_merkle_link / +// make_reconstruct_closure test (#744 broadcaster arc, faithful as_block +// composition + run-loop closure -- slice 5/7). +// +// Slice 4 (block_assembly_test.cpp) pinned the FRAMING half (merkle-root +// reconstruction + [gentx]++other_txs ordering + body/header fail-closed guard + +// conditional witness codec). This slice pins the COMPOSITION that feeds it: +// * select_won_block_merkle_link -- the SEGWIT link selection SSOT +// (share_check.hpp:674-692): segwit-activated shares with segwit_data walk +// segwit_data.txid_merkle_link, legacy (or segwit-active-but-no-segwit_data) +// shares walk merkle_link. The framer takes an ALREADY-resolved link, so the +// selection is exercised HERE, in the caller; +// * reconstruct_won_block -- select link + assemble_won_block, round-tripping +// through the live BlockType wire codec, with the captured GBT template as +// the non-coinbase tx source (NOT tx_hash_refs); +// * make_reconstruct_closure -- the run-loop WonBlockReconstructor: fail-closed +// (std::nullopt, never throw) on a malformed gentx or a body/header mismatch, +// and WIRED end-to-end into make_on_block_found (won_block_dispatch.hpp) so +// the dispatch dual-path carries the reconstructed block. +// +// Fixtures mirror block_assembly_test.cpp (self-derived via the same Hash() the +// merkle walk uses, independent of any fixture file). Rides the already- +// allowlisted btc_share_test executable, so no build.yml --target change is +// needed. p2pool-merged-v36 surface: NONE. +// --------------------------------------------------------------------------- + +#include + +#include +#include +#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::other_tx_txid; +using btc::coin::ReconstructedWonBlock; +using btc::coin::WonShareReconstructFields; +using btc::coin::reconstruct_won_block; +using btc::coin::select_won_block_merkle_link; +using btc::coin::make_reconstruct_closure; +using btc::coin::make_on_block_found; +using btc::coin::unpack_gentx_coinbase; + +constexpr uint64_t SEGWIT_VER = btc::PoolConfig::SEGWIT_ACTIVATION_VERSION; // 33 +constexpr uint64_t LEGACY_VER = btc::PoolConfig::SEGWIT_ACTIVATION_VERSION - 1; // 32 + +// --- fixtures (identical shapes to block_assembly_test.cpp) ------------------- +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; + h.m_previous_block.SetHex("00000000000000000000000000000000000000000000000000000000deadbeef"); + h.m_timestamp = 1718700000; + h.m_bits = 0x1a0fffff; + h.m_nonce = 0x12345678; + return h; +} + +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, false); } + +// The non-witness gentx bytes the SSOT serializer would hand the closure. +// NB: keep the PackStream in a NAMED local -- get_span() aliases its buffer, so +// a `pack(...).get_span()` temporary would dangle (cf. block_assembly.hpp). +std::vector gentx_nonwitness_bytes(const MutableTransaction& tx) +{ + auto packed = pack(btc::coin::TX_NO_WITNESS(tx)); + auto sp = packed.get_span(); + return std::vector( + reinterpret_cast(sp.data()), + reinterpret_cast(sp.data()) + sp.size()); +} + +uint256 gentx_txid(const MutableTransaction& tx) +{ + return Hash(pack(btc::coin::TX_NO_WITNESS(tx)).get_span()); +} + +::btc::MerkleLink one_branch_link(const uint256& sibling) +{ + ::btc::MerkleLink link; + link.m_branch.push_back(sibling); + link.m_index = 0; + return link; +} + +std::string to_hex_lower(const std::vector& b) +{ + static const char* d = "0123456789abcdef"; + std::string s; + s.reserve(b.size() * 2); + for (unsigned char c : b) { s.push_back(d[c >> 4]); s.push_back(d[c & 0xf]); } + return s; +} + +// ============================================================================= +// select_won_block_merkle_link -- the segwit selection SSOT +// ============================================================================= + +// --- Test 1: segwit-active share WITH segwit_data => txid_merkle_link ---------- +TEST(BtcReconstructLinkSelect, SegwitPicksTxidLink) +{ + uint256 legacy_sib; legacy_sib.SetHex("1111111111111111111111111111111111111111111111111111111111111111"); + uint256 segwit_sib; segwit_sib.SetHex("2222222222222222222222222222222222222222222222222222222222222222"); + auto merkle_link = one_branch_link(legacy_sib); + std::optional<::btc::MerkleLink> txid_link = one_branch_link(segwit_sib); + + const auto& picked = select_won_block_merkle_link(SEGWIT_VER, merkle_link, txid_link); + ASSERT_EQ(picked.m_branch.size(), 1u); + EXPECT_EQ(picked.m_branch[0], segwit_sib); // the txid link, not the legacy one +} + +// --- Test 2: legacy share => merkle_link, even if a txid link is present ------- +TEST(BtcReconstructLinkSelect, LegacyPicksMerkleLink) +{ + uint256 legacy_sib; legacy_sib.SetHex("1111111111111111111111111111111111111111111111111111111111111111"); + uint256 segwit_sib; segwit_sib.SetHex("2222222222222222222222222222222222222222222222222222222222222222"); + auto merkle_link = one_branch_link(legacy_sib); + std::optional<::btc::MerkleLink> txid_link = one_branch_link(segwit_sib); + + const auto& picked = select_won_block_merkle_link(LEGACY_VER, merkle_link, txid_link); + ASSERT_EQ(picked.m_branch.size(), 1u); + EXPECT_EQ(picked.m_branch[0], legacy_sib); +} + +// --- Test 3: segwit-active but NO segwit_data (sentinel) => merkle_link -------- +TEST(BtcReconstructLinkSelect, SegwitActiveNoSegwitDataPicksMerkleLink) +{ + uint256 legacy_sib; legacy_sib.SetHex("1111111111111111111111111111111111111111111111111111111111111111"); + auto merkle_link = one_branch_link(legacy_sib); + std::optional<::btc::MerkleLink> none = std::nullopt; + + const auto& picked = select_won_block_merkle_link(SEGWIT_VER, merkle_link, none); + ASSERT_EQ(picked.m_branch.size(), 1u); + EXPECT_EQ(picked.m_branch[0], legacy_sib); +} + +// ============================================================================= +// reconstruct_won_block -- select link + frame, round-trip through BlockType +// ============================================================================= + +// --- Test 4: coinbase-only (empty template) round-trips, single-tx block ------- +TEST(BtcReconstructBody, CoinbaseOnlyRoundTrips) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gid = gentx_txid(gentx); + ::btc::MerkleLink empty; // empty branch => header root == gentx txid + + auto r = reconstruct_won_block(sh, LEGACY_VER, empty, std::nullopt, gentx, gid, {}); + EXPECT_EQ(r.hex, HexStr(std::span( + reinterpret_cast(r.bytes.data()), r.bytes.size()))); + + PackStream ps(r.bytes); + BlockType blk; + ps >> blk; + EXPECT_EQ(blk.m_merkle_root, gid); + ASSERT_EQ(blk.m_txs.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vout[0].value, gentx.vout[0].value); +} + +// --- Test 5: one template tx, [gentx]++other order, consistent link ----------- +TEST(BtcReconstructBody, WithTemplateTxOrdered) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gid = gentx_txid(gentx); + auto tx1 = make_tx(10); + auto t1id = other_tx_txid(tx1); + auto link = one_branch_link(t1id); // leaf-0 branch for [gentx, t1] + + auto r = reconstruct_won_block(sh, LEGACY_VER, link, std::nullopt, gentx, gid, {tx1}); + PackStream ps(r.bytes); + BlockType blk; + ps >> blk; + EXPECT_EQ(blk.m_merkle_root, pair_hash(gid, t1id)); + ASSERT_EQ(blk.m_txs.size(), 2u); + EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); // coinbase index 0 + EXPECT_EQ(blk.m_txs[1].vout[0].value, 10); +} + +// --- Test 6: the SEGWIT link is the one actually walked (selection is live) ---- +// Build a share whose header root was committed over the txid_merkle_link. The +// legacy merkle_link is deliberately a garbage branch: if reconstruct wrongly +// used it, the framer's body/header guard would throw. Success proves the segwit +// link was selected. +TEST(BtcReconstructBody, SegwitLinkIsWalkedNotLegacy) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gid = gentx_txid(gentx); + auto tx1 = make_tx(77); + auto t1id = other_tx_txid(tx1); + + ::btc::MerkleLink good_txid_link = one_branch_link(t1id); // matches body + uint256 garbage; garbage.SetHex("dead00000000000000000000000000000000000000000000000000000000beef"); + ::btc::MerkleLink bad_legacy_link = one_branch_link(garbage); // would mismatch + + auto r = reconstruct_won_block(sh, SEGWIT_VER, bad_legacy_link, + std::optional<::btc::MerkleLink>(good_txid_link), + gentx, gid, {tx1}); + PackStream ps(r.bytes); + BlockType blk; + ps >> blk; + EXPECT_EQ(blk.m_merkle_root, pair_hash(gid, t1id)); + ASSERT_EQ(blk.m_txs.size(), 2u); +} + +// --- Test 7: body/header mismatch propagates as a throw from the body ---------- +TEST(BtcReconstructBody, MismatchThrows) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gid = gentx_txid(gentx); + ::btc::MerkleLink empty; // header commits coinbase-only, but body has a fee tx + EXPECT_THROW( + reconstruct_won_block(sh, LEGACY_VER, empty, std::nullopt, gentx, gid, {make_tx(5)}), + std::runtime_error); +} + +// ============================================================================= +// make_reconstruct_closure -- the fail-closed run-loop WonBlockReconstructor +// ============================================================================= + +// --- Test 8: success path returns bytes == direct reconstruct_won_block -------- +TEST(BtcReconstructClosure, SuccessMatchesDirectReconstruct) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gid = gentx_txid(gentx); + auto tx1 = make_tx(10); + auto t1id = other_tx_txid(tx1); + auto link = one_branch_link(t1id); + + auto fields_fn = [&](const uint256&) { + WonShareReconstructFields f; + f.small_header = sh; + f.share_version = LEGACY_VER; + f.merkle_link = link; + f.txid_merkle_link = std::nullopt; + return f; + }; + auto gentx_fn = [&](const uint256&) { return gentx_nonwitness_bytes(gentx); }; + auto tmpl_fn = [&](const uint256&) { return std::vector{tx1}; }; + + auto recon = make_reconstruct_closure(fields_fn, gentx_fn, tmpl_fn); + auto out = recon(uint256::ZERO); + + ASSERT_TRUE(out.has_value()); + auto direct = reconstruct_won_block(sh, LEGACY_VER, link, std::nullopt, gentx, gid, {tx1}); + EXPECT_EQ(out->first, direct.bytes); + EXPECT_EQ(out->second, direct.hex); + EXPECT_EQ(out->second, to_hex_lower(out->first)); +} + +// --- Test 9: malformed gentx (trailing bytes) => FAIL CLOSED (nullopt) --------- +TEST(BtcReconstructClosure, MalformedGentxFailsClosed) +{ + auto sh = make_small_header(); + auto fields_fn = [&](const uint256&) { + WonShareReconstructFields f; + f.small_header = sh; + f.share_version = LEGACY_VER; + return f; + }; + // Append a trailing byte so unpack_gentx_coinbase throws std::out_of_range. + auto bad_bytes = gentx_nonwitness_bytes(make_gentx()); + bad_bytes.push_back(0x00); + auto gentx_fn = [&](const uint256&) { return bad_bytes; }; + auto tmpl_fn = [&](const uint256&) { return std::vector{}; }; + + auto recon = make_reconstruct_closure(fields_fn, gentx_fn, tmpl_fn); + EXPECT_FALSE(recon(uint256::ZERO).has_value()); // never throws out of the callback +} + +// --- Test 10: body/header mismatch => FAIL CLOSED (nullopt), not a throw ------- +TEST(BtcReconstructClosure, MerkleMismatchFailsClosed) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto fields_fn = [&](const uint256&) { + WonShareReconstructFields f; + f.small_header = sh; + f.share_version = LEGACY_VER; + f.merkle_link = ::btc::MerkleLink{}; // empty => coinbase-only header root + f.txid_merkle_link = std::nullopt; + return f; + }; + auto gentx_fn = [&](const uint256&) { return gentx_nonwitness_bytes(gentx); }; + // But the template carries a fee tx => body root != header root => guard throws. + auto tmpl_fn = [&](const uint256&) { return std::vector{make_tx(9)}; }; + + auto recon = make_reconstruct_closure(fields_fn, gentx_fn, tmpl_fn); + EXPECT_FALSE(recon(uint256::ZERO).has_value()); +} + +// ============================================================================= +// WIRING: make_reconstruct_closure -> make_on_block_found -> BOTH dual-path arms +// ============================================================================= + +// --- Test 11: the closure drives the dispatch handler end-to-end --------------- +TEST(BtcReconstructWiring, ClosureDrivesDispatchDualPath) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gid = gentx_txid(gentx); + ::btc::MerkleLink empty; + + auto fields_fn = [&](const uint256&) { + WonShareReconstructFields f; + f.small_header = sh; + f.share_version = LEGACY_VER; + f.merkle_link = empty; + f.txid_merkle_link = std::nullopt; + return f; + }; + auto gentx_fn = [&](const uint256&) { return gentx_nonwitness_bytes(gentx); }; + auto tmpl_fn = [&](const uint256&) { return std::vector{}; }; + + auto recon = make_reconstruct_closure(fields_fn, gentx_fn, tmpl_fn); + + std::vector relayed; + bool did_relay = false; + std::string submitted; + int submit_calls = 0; + auto relay = [&](const std::vector& b) { did_relay = true; relayed = b; return true; }; + auto submit = [&](const std::string& h) { ++submit_calls; submitted = h; return true; }; + + auto handler = make_on_block_found(recon, relay, submit); + handler(uint256::ZERO); // FORCE the won share through the real reconstructor + + // Both arms fired, carrying the byte-identical reconstructed block. + auto direct = reconstruct_won_block(sh, LEGACY_VER, empty, std::nullopt, gentx, gid, {}); + ASSERT_TRUE(did_relay); + EXPECT_EQ(relayed, direct.bytes); + ASSERT_EQ(submit_calls, 1); + EXPECT_EQ(submitted, direct.hex); + EXPECT_EQ(to_hex_lower(relayed), submitted); // cross-arm identity +} + +// --- Test 12: a malformed won share reconstructs to nullopt => NEITHER arm ----- +TEST(BtcReconstructWiring, FailClosedShareBroadcastsNothing) +{ + auto fields_fn = [&](const uint256&) { + WonShareReconstructFields f; + f.share_version = LEGACY_VER; + return f; + }; + auto bad_bytes = gentx_nonwitness_bytes(make_gentx()); + bad_bytes.push_back(0x00); // trailing byte => unpack throws + auto gentx_fn = [&](const uint256&) { return bad_bytes; }; + auto tmpl_fn = [&](const uint256&) { return std::vector{}; }; + + auto recon = make_reconstruct_closure(fields_fn, gentx_fn, tmpl_fn); + + bool did_relay = false; + int submit_calls = 0; + auto relay = [&](const std::vector&) { did_relay = true; return true; }; + auto submit = [&](const std::string&) { ++submit_calls; return true; }; + + auto handler = make_on_block_found(recon, relay, submit); + handler(uint256::ZERO); + + EXPECT_FALSE(did_relay); + EXPECT_EQ(submit_calls, 0); +} + +} // namespace