From 08421941288d163787f390812bf42e1a8cea8c4f Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sat, 20 Jun 2026 13:02:49 +0000 Subject: [PATCH] dgb(#82): reconstruct won block from captured GBT template, not share refs The won-block broadcast tx set is the GBT template snapshot the miner was handed at job hand-out (current_work transactions[]), NOT the share's transaction_hash_refs. Confirmed against p2pool-merged-v36/p2pool/work.py @ 42ccca53 (transactions=current_work.value[transactions]; other_transactions from tx_map; submit txs=[gentx]+[tx.data]). The ref-walk path is a share-chain peer-propagation mechanism, never the block source -- which makes assembly version-agnostic (pre-v34 vs v34+/v36 irrelevant; the share never carried the block tx set), dissolving the v34+ m_tx_info reconstruct concern. Add reconstruct_won_block_from_template(small_header, merkle_link, gentx, gentx_hash, template_other_txs) framing [gentx]++template_other_txs via the assemble_won_block SSOT. Empty template (today's embedded path until mempool tx-selection wires) yields a valid coinbase-only block, not a fail-closed nullopt. +2 KATs: template-path == ref-path for the same tx set (pure source swap), and empty-template gentx-only. dgb_reconstruct_won_block_test 7/7. --- src/impl/dgb/coin/reconstruct_won_block.hpp | 43 +++++++++++++ .../dgb/test/reconstruct_won_block_test.cpp | 60 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/impl/dgb/coin/reconstruct_won_block.hpp b/src/impl/dgb/coin/reconstruct_won_block.hpp index 26324577f..da4fcfd9b 100644 --- a/src/impl/dgb/coin/reconstruct_won_block.hpp +++ b/src/impl/dgb/coin/reconstruct_won_block.hpp @@ -123,5 +123,48 @@ reconstruct_won_block( return ReconstructedWonBlock{std::move(framed.first), std::move(framed.second)}; } +// --------------------------------------------------------------------------- +// reconstruct_won_block_from_template -- the CORRECT won-block tx source, per +// the p2pool-merged-v36 block-assembly audit (work.py @ 42ccca53): +// +// transactions = self.current_work.value['transactions'] # ~2486 +// tx_map = dict(zip(transaction_hashes, transactions)) # ~2489 +// other_transactions = [tx_map[h] for h in other_transaction_hashes] # ~2638 +// submit txs = [new_hexed_gentx] +// + [tx['data'] for tx in other_transactions] # ~2728 +// +// The broadcast block's non-coinbase tx set is the GBT TEMPLATE snapshot the +// miner was handed at job hand-out time (current_work), NOT the share's +// transaction_hash_refs. The ref-walk above (reconstruct_won_block) 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 (this dissolves the "v34+ carries no m_tx_info -> reconstruct fails" +// concern -- there is nothing to source from the share for any version). +// +// template_other_txs : the captured template's non-coinbase txs, in template +// order (current_work transactions[]). Empty today -- +// the embedded path emits transactions[]==[] until +// mempool tx-selection wires -- which yields a valid +// coinbase-only block (correct-and-empty, NOT +// fail-closed). It fills automatically as tx-selection +// lands, with no change to this seam. +// +// Frames [gentx] ++ template_other_txs via the same assemble_won_block SSOT +// (identical merkle-root math + BlockType codec the live submitblock path +// uses), so the result round-trips and the daemon accepts. +inline ReconstructedWonBlock +reconstruct_won_block_from_template( + const SmallBlockHeaderType& small_header, + const ::dgb::MerkleLink& merkle_link, + const MutableTransaction& gentx, + const uint256& gentx_hash, + const std::vector& template_other_txs) +{ + auto framed = assemble_won_block(small_header, gentx, gentx_hash, merkle_link, + template_other_txs); + return ReconstructedWonBlock{std::move(framed.first), std::move(framed.second)}; +} + } // namespace coin } // namespace dgb diff --git a/src/impl/dgb/test/reconstruct_won_block_test.cpp b/src/impl/dgb/test/reconstruct_won_block_test.cpp index e4a4418d1..bc737373e 100644 --- a/src/impl/dgb/test/reconstruct_won_block_test.cpp +++ b/src/impl/dgb/test/reconstruct_won_block_test.cpp @@ -32,6 +32,7 @@ #include "../coin/reconstruct_won_block.hpp" using dgb::coin::reconstruct_won_block; +using dgb::coin::reconstruct_won_block_from_template; using dgb::coin::assemble_won_block; using dgb::coin::resolve_other_tx_hashes; using dgb::coin::assemble_other_txs; @@ -254,4 +255,63 @@ TEST(DgbReconstructWonBlock, UnknownKnownTxThrows) std::out_of_range); } +// --- Test 5: template path == ref path for the SAME tx set (equivalence) ------ +// Proves reconstruct_won_block_from_template (the CORRECT block-broadcast +// source: captured GBT template txs) frames a byte-identical block to the +// ref-walk path when fed the same non-coinbase tx set -- i.e. the re-scope is a +// pure SOURCE swap, the framing/merkle/codec are unchanged. +TEST(DgbReconstructWonBlock, TemplatePathMatchesRefPathForSameTxSet) +{ + Fixture f; + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gh = fixed_gentx_hash(); + ::dgb::MerkleLink link; + std::vector refs = { TxHashRefs(0, 1), TxHashRefs(1, 0) }; + + // Ref-walk path: resolve hashes -> assemble the other_txs the share refs to. + auto hashes = resolve_other_tx_hashes(f.won, refs, f.nth_parent(), f.new_tx()); + auto template_txs = assemble_other_txs(hashes, f.known_fn()); + + auto ref_got = reconstruct_won_block(sh, link, gentx, gh, f.won, refs, + f.nth_parent(), f.new_tx(), f.known_fn()); + auto tmpl_got = reconstruct_won_block_from_template(sh, link, gentx, gh, + template_txs); + + EXPECT_EQ(tmpl_got.bytes, ref_got.bytes); + EXPECT_EQ(tmpl_got.hex, ref_got.hex); + + PackStream ps(tmpl_got.bytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 3u); // gentx + 2 template txs + EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); + EXPECT_EQ(blk.m_txs[1].vout[0].value, 11); // template order preserved + EXPECT_EQ(blk.m_txs[2].vout[0].value, 22); +} + +// --- Test 6: empty captured template => valid coinbase-only block ------------- +// Today's embedded path emits transactions[]==[]; reconstruct must yield a +// valid gentx-only block (correct-and-empty), never throw / fail closed. +TEST(DgbReconstructWonBlock, EmptyTemplateGentxOnly) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gh = fixed_gentx_hash(); + ::dgb::MerkleLink link; + std::vector template_txs; // captured template was empty + + auto got = reconstruct_won_block_from_template(sh, link, gentx, gh, template_txs); + + PackStream ps(got.bytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); + EXPECT_EQ(blk.m_merkle_root, gh); // empty link => root == gentx_hash + auto sp = std::span( + reinterpret_cast(got.bytes.data()), got.bytes.size()); + EXPECT_EQ(got.hex, HexStr(sp)); +} + } // namespace