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