diff --git a/src/impl/dgb/coin/reconstruct_closure.hpp b/src/impl/dgb/coin/reconstruct_closure.hpp index 9a0453f05..cb1bdbe77 100644 --- a/src/impl/dgb/coin/reconstruct_closure.hpp +++ b/src/impl/dgb/coin/reconstruct_closure.hpp @@ -56,6 +56,7 @@ #include "reconstruct_won_block.hpp" // reconstruct_won_block, ReconstructedWonBlock, SmallBlockHeaderType, MutableTransaction #include "gentx_unpack.hpp" // unpack_gentx_coinbase, UnpackedGentx #include "won_block_dispatch.hpp" // WonBlockReconstructor +#include "won_share_inputs.hpp" // WonShareInputs, won_share_inputs (#279) namespace dgb { @@ -122,5 +123,81 @@ make_reconstruct_closure( }; } + +// --------------------------------------------------------------------------- +// make_reconstruct_closure_from_template -- the version-AGNOSTIC #82 closure, +// and the one the run-loop should install. reconstruct_won_block_from_template +// (reconstruct_won_block.hpp) is the CORRECT won-block tx source per the +// work.py @42ccca53 audit: the broadcast block's non-coinbase set is the GBT +// TEMPLATE the miner was handed at job hand-out (current_work transactions[]), +// NOT the share's transaction_hash_refs. That dissolves the "v34+ carries no +// m_tx_info -> reconstruct fails" concern entirely -- the share never carried +// the block tx set for ANY version. This builder composes the three version- +// agnostic inputs into the run-loop WonBlockReconstructor: +// won_share_fields_fn = won_share_inputs(chain.get_share(h)) (#279) -> +// { small_header = share.m_min_header, +// merkle_link = share.m_merkle_link } +// gentx_bytes_fn = generate_share_transaction(share,...).bytes (#173) +// -> unpack_gentx_coinbase -> { tx, txid } +// template_other_txs_fn = the captured-GBT template's non-coinbase txs in +// template order (#271). Empty today (mempool tx- +// selection pending) => a valid coinbase-only block, +// correct-and-empty, NOT fail-closed. It fills with +// NO change to this seam as tx-selection lands. +// +// Same FAIL-CLOSED posture as make_reconstruct_closure: fires on the compute +// thread for a won share, NEVER throws and NEVER emits a partial/wrong block -- +// any error is caught, logged LOUDLY, and yields std::nullopt (announce + +// audit; the RPC submitblock fallback still attempts independently). +// +// Prefer THIS over make_reconstruct_closure for the run-loop install: the +// ref-walk variant is a share-CHAIN peer-propagation mechanism, never the +// block-broadcast source. Per-coin isolation: src/impl/dgb/ only. +// p2pool-merged-v36 surface: NONE. +// --------------------------------------------------------------------------- +inline WonBlockReconstructor +make_reconstruct_closure_from_template( + std::function won_share_fields_fn, + std::function(const uint256&)> gentx_bytes_fn, + std::function(const uint256&)> template_other_txs_fn) +{ + return [won_share_fields_fn = std::move(won_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-side inputs the won share carries verbatim (#279). + const WonShareInputs si = won_share_fields_fn(share_hash); + + // 2. regenerate + unpack the share's SSOT gentx (block tx index 0). + const UnpackedGentx ug = unpack_gentx_coinbase(gentx_bytes_fn(share_hash)); + + // 3. the captured-GBT template's non-coinbase set (template order). + const std::vector other_txs = + template_other_txs_fn(share_hash); + + // 4. frame [gentx] ++ template_other_txs via the assemble_won_block + // SSOT (version-agnostic; empty other_txs => coinbase-only). + ReconstructedWonBlock r = reconstruct_won_block_from_template( + si.small_header, si.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 RPC submitblock fallback still attempts independently. + std::cout << "[DGB-POOL-BLOCK] won share " << share_hash.GetHex().substr(0, 16) + << " -- reconstruct (from-template) FAILED CLOSED (" << e.what() + << "); NOT broadcast on P2P arm (RPC fallback still attempts)" + << std::endl; + return std::nullopt; + } + }; +} + } // namespace coin } // namespace dgb diff --git a/src/impl/dgb/test/reconstruct_closure_test.cpp b/src/impl/dgb/test/reconstruct_closure_test.cpp index fee934a42..1f41b2643 100644 --- a/src/impl/dgb/test/reconstruct_closure_test.cpp +++ b/src/impl/dgb/test/reconstruct_closure_test.cpp @@ -41,6 +41,9 @@ using dgb::coin::unpack_gentx_coinbase; using dgb::coin::SmallBlockHeaderType; using dgb::coin::MutableTransaction; using dgb::TxHashRefs; +using dgb::coin::make_reconstruct_closure_from_template; +using dgb::coin::reconstruct_won_block_from_template; +using dgb::coin::WonShareInputs; namespace { @@ -276,3 +279,120 @@ TEST(DgbReconstructClosure, MalformedGentxBytesFailsClosed) EXPECT_FALSE(closure(f.won).has_value()); } + + +// ============================================================================= +// from-template closure (#82 version-AGNOSTIC path, #271 source + #279 share +// fields). This is the closure the run-loop installs: its non-coinbase tx set +// is the captured GBT template, NOT the share's tx_hash_refs, so there is no +// ancestry walk and no known-tx store -- only the two share-side fields, the +// gentx, and the template's other txs. +// ============================================================================= + +// won_share_fields_fn knows ONLY the won share; throws for any other hash +// (the run-loop's chain.get_share miss). +static std::function +won_fields(const uint256& won, SmallBlockHeaderType sh, ::dgb::MerkleLink link) +{ + return [won, sh, link](const uint256& h) -> WonShareInputs { + if (h != won) throw std::out_of_range("won_share_fields: unknown share"); + return WonShareInputs{sh, link}; + }; +} + +// --- Test 7: from-template success == reconstruct_won_block_from_template ----- +TEST(DgbReconstructClosure, FromTemplateSuccessComposesIdenticalBlock) +{ + auto sh = make_small_header(); + ::dgb::MerkleLink link; + uint256 won = H("a0"); + auto gentx_bytes = noseg_bytes(make_gentx()); + auto ug = unpack_gentx_coinbase(gentx_bytes); + std::vector other = { make_tx(11), make_tx(22) }; + + auto expected = + reconstruct_won_block_from_template(sh, link, ug.tx, ug.txid, other); + + auto closure = make_reconstruct_closure_from_template( + won_fields(won, sh, link), + [gentx_bytes](const uint256&) { return gentx_bytes; }, + [other](const uint256&) { return other; }); + + auto got = closure(won); + ASSERT_TRUE(got.has_value()); + EXPECT_FALSE(got->first.empty()); + EXPECT_EQ(got->first, expected.bytes); // closure adds seams, not bytes + EXPECT_EQ(got->second, expected.hex); +} + +// --- Test 8: empty template => valid coinbase-only block (NOT fail-closed) ---- +TEST(DgbReconstructClosure, FromTemplateEmptyOtherTxsIsCoinbaseOnly) +{ + auto sh = make_small_header(); + ::dgb::MerkleLink link; + uint256 won = H("a0"); + auto gentx_bytes = noseg_bytes(make_gentx()); + auto ug = unpack_gentx_coinbase(gentx_bytes); + + auto expected = reconstruct_won_block_from_template( + sh, link, ug.tx, ug.txid, std::vector{}); + + auto closure = make_reconstruct_closure_from_template( + won_fields(won, sh, link), + [gentx_bytes](const uint256&) { return gentx_bytes; }, + [](const uint256&) { return std::vector{}; }); + + auto got = closure(won); + ASSERT_TRUE(got.has_value()); // correct-and-empty, not nullopt + EXPECT_EQ(got->first, expected.bytes); +} + +// --- Test 9: unknown share => nullopt (won_share_fields miss) ----------------- +TEST(DgbReconstructClosure, FromTemplateUnknownShareFailsClosed) +{ + auto sh = make_small_header(); + ::dgb::MerkleLink link; + uint256 won = H("a0"); + auto gentx_bytes = noseg_bytes(make_gentx()); + + auto closure = make_reconstruct_closure_from_template( + won_fields(won, sh, link), + [gentx_bytes](const uint256&) { return gentx_bytes; }, + [](const uint256&) { return std::vector{}; }); + + EXPECT_FALSE(closure(H("ff")).has_value()); // not the won share +} + +// --- Test 10: malformed gentx bytes => nullopt (unpack out_of_range) ---------- +TEST(DgbReconstructClosure, FromTemplateMalformedGentxFailsClosed) +{ + auto sh = make_small_header(); + ::dgb::MerkleLink link; + uint256 won = H("a0"); + auto bad = noseg_bytes(make_gentx()); + bad.push_back(0xff); // trailing byte => unpack throws + + auto closure = make_reconstruct_closure_from_template( + won_fields(won, sh, link), + [bad](const uint256&) { return bad; }, + [](const uint256&) { return std::vector{}; }); + + EXPECT_FALSE(closure(won).has_value()); +} + +// --- Test 11: template source throws (non-out_of_range) => still nullopt ------ +TEST(DgbReconstructClosure, FromTemplateOtherTxsThrowFailsClosed) +{ + auto sh = make_small_header(); + ::dgb::MerkleLink link; + uint256 won = H("a0"); + auto gentx_bytes = noseg_bytes(make_gentx()); + + auto closure = make_reconstruct_closure_from_template( + won_fields(won, sh, link), + [gentx_bytes](const uint256&) { return gentx_bytes; }, + [](const uint256&) -> std::vector { + throw std::runtime_error("template boom"); }); + + EXPECT_FALSE(closure(won).has_value()); // broad catch, not just out_of_range +}