diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf5e3f8c0..3cd78132e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,7 +82,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_coinbase_value_parity_test dgb_submit_classify_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test v37_test \ -j$(nproc) @@ -214,7 +214,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_coinbase_value_parity_test dgb_submit_classify_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ diff --git a/src/impl/dgb/coin/template_other_txs.hpp b/src/impl/dgb/coin/template_other_txs.hpp new file mode 100644 index 000000000..affd088fb --- /dev/null +++ b/src/impl/dgb/coin/template_other_txs.hpp @@ -0,0 +1,108 @@ +// --------------------------------------------------------------------------- +// template_other_txs.hpp -- the producer bridge between the embedded work +// template's transactions[] (make_mempool_tx_source / embedded_tx_select.cpp) +// and the won-block reconstructor's template_other_txs_fn seam +// (reconstruct_closure.hpp, make_reconstruct_closure_from_template). +// +// The reconstructor frames the broadcast block as [gentx] ++ other_txs, where +// other_txs is the captured-GBT template's non-coinbase set in template order +// (#271). The template carries each tx as a GBT entry {data,txid,hash,fee}; the +// reconstructor consumes them as already-deserialized dgb::coin::Mutable- +// Transaction. This header is the missing deserialize step: the GBT `data` +// (with-witness) hex -> MutableTransaction, in template order. It is the literal +// wire between make_mempool_tx_source (the template's tx SOURCE) and the +// reconstructor (the broadcast tx SINK). +// +// SSOT split: +// * deserialize_template_other_txs(json) -- pure: transactions[] -> txs[]. +// Throws on a malformed `data` entry (bad hex / trailing bytes) so the +// closure's broad catch fails the whole won block CLOSED rather than +// broadcasting a half-decoded tx set (mirrors unpack_gentx_coinbase). +// * make_template_other_txs_fn(captured_template_txs_fn) -- adapts a per-share +// captured-transactions[] provider into the template_other_txs_fn signature +// make_reconstruct_closure_from_template installs. The provider MUST return +// the transactions[] of the GBT TEMPLATE the won share was handed at job +// hand-out (the per-job capture seam, #271), NOT the live mempool selection: +// a won share commits to the template it was mined against, which may differ +// from the current mempool. Wiring that per-job capture provider is the +// run-loop integration; this header owns ONLY the deserialize wire. +// +// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE -- the +// transactions[] form is already the conformant GBT shape make_mempool_tx_source +// emits; this only decodes it back into the in-memory tx the block carries. +// --------------------------------------------------------------------------- +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include // ParseHex + +#include "transaction.hpp" // MutableTransaction, UnserializeTransaction, TX_WITH_WITNESS + +namespace dgb +{ +namespace coin +{ + +// Decode one GBT `data` (with-witness) hex string into a MutableTransaction. +// Throws std::out_of_range if the hex carries trailing bytes past a complete +// transaction (a malformed template entry), matching unpack_gentx_coinbase's +// fail-closed contract for the gentx. +inline MutableTransaction +deserialize_template_tx(const std::string& data_hex) +{ + PackStream ps(ParseHex(data_hex)); + MutableTransaction tx; + // With-witness parse: make_mempool_tx_source emits `data` as the with- + // witness submit bytes (pack(TX_WITH_WITNESS(tx))), so decode symmetrically. + UnserializeTransaction(tx, ps, TX_WITH_WITNESS); + if (!ps.empty()) + throw std::out_of_range( + "deserialize_template_tx: trailing bytes after tx -- " + "malformed GBT template `data` entry"); + return tx; +} + +// Decode the captured GBT template's transactions[] into the won-block other-tx +// vector, in template order (block tx order after the coinbase). Each entry's +// `data` field is the with-witness bytes make_mempool_tx_source emitted. An +// empty / absent array yields an empty vector (a valid coinbase-only block). +inline std::vector +deserialize_template_other_txs(const nlohmann::json& transactions) +{ + std::vector out; + if (transactions.is_array()) + { + out.reserve(transactions.size()); + for (const auto& entry : transactions) + out.push_back(deserialize_template_tx(entry.at("data").get())); + } + return out; +} + +// Adapt a per-share captured-transactions[] provider into the run-loop's +// template_other_txs_fn (the third argument of make_reconstruct_closure_from_ +// template). captured_template_txs_fn(share_hash) MUST return the transactions[] +// of the template the won share was mined against (per-job capture, #271); +// decoded here through the deserialize SSOT. Any decode error propagates so the +// reconstruct closure fails the won block CLOSED. +inline std::function(const uint256&)> +make_template_other_txs_fn( + std::function captured_template_txs_fn) +{ + return [captured_template_txs_fn = std::move(captured_template_txs_fn)]( + const uint256& share_hash) -> std::vector { + return deserialize_template_other_txs(captured_template_txs_fn(share_hash)); + }; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index af10c1792..ada041fde 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -259,6 +259,21 @@ if (BUILD_TESTING AND GTest_FOUND) nlohmann_json::nlohmann_json) gtest_add_tests(dgb_embedded_tx_select_test "" AUTO) + # dgb_template_other_txs_test: pins coin/template_other_txs.hpp, the producer + # bridge decoding the embedded template transactions[] (make_mempool_tx_source + # GBT data) back into the MutableTransaction vector the won-block reconstructor + # frames as [gentx] ++ other_txs. Compiles the tx codec AND the reconstruct + # closure, so it links the embedded_tx_select SCC set + pool/sharechain. MUST + # also be in BOTH build.yml --target allowlists (#143 NOT_BUILT trap). + add_executable(dgb_template_other_txs_test template_other_txs_test.cpp) + target_link_libraries(dgb_template_other_txs_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb dgb_stratum + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain + nlohmann_json::nlohmann_json) + gtest_add_tests(dgb_template_other_txs_test "" AUTO) + # dgb_coinbase_value_parity_test: END-TO-END GBT-parity KAT. Drives a # populated Mempool through the production make_mempool_tx_source -> # EmbeddedCoinNode -> build_work_template path and asserts coinbasevalue == diff --git a/src/impl/dgb/test/template_other_txs_test.cpp b/src/impl/dgb/test/template_other_txs_test.cpp new file mode 100644 index 000000000..9ae96d3c3 --- /dev/null +++ b/src/impl/dgb/test/template_other_txs_test.cpp @@ -0,0 +1,232 @@ +// --------------------------------------------------------------------------- +// dgb_template_other_txs_test -- pins coin/template_other_txs.hpp, the producer +// bridge that decodes the embedded work template's transactions[] (the GBT data +// the miner is funded with, shaped by make_mempool_tx_source) back into the +// MutableTransaction vector the won-block reconstructor's template_other_txs_fn +// seam frames as [gentx] ++ other_txs. +// +// This closes the loop between the two already-pinned halves: +// * make_mempool_tx_source (embedded_tx_select.cpp) -- Mempool -> GBT txs[] +// * make_reconstruct_closure_from_template (#82) -- [gentx] ++ other_txs +// proving the SAME txs the template hands the miner are the txs that land in the +// reconstructed broadcast block, byte-faithfully and in template order. +// +// Links the full dgb_coin codec (it compiles the tx serialization) and the +// reconstruct closure. MUST be in BOTH build.yml --target allowlists (#143 +// NOT_BUILT trap). Per-coin isolation: src/impl/dgb/ only. +// --------------------------------------------------------------------------- +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +using dgb::coin::Mempool; +using dgb::coin::MutableTransaction; +using dgb::coin::TxIn; +using dgb::coin::TxOut; +using dgb::coin::TX_WITH_WITNESS; +using dgb::coin::TX_NO_WITNESS; +using dgb::coin::compute_txid; +using dgb::coin::make_mempool_tx_source; +using dgb::coin::deserialize_template_tx; +using dgb::coin::deserialize_template_other_txs; +using dgb::coin::make_template_other_txs_fn; +using dgb::coin::make_reconstruct_closure_from_template; +using dgb::coin::reconstruct_won_block_from_template; +using dgb::coin::unpack_gentx_coinbase; +using dgb::coin::SmallBlockHeaderType; +using dgb::coin::WonShareInputs; + +namespace { + +MutableTransaction tagged_tx(int64_t value, uint32_t index) +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = index; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + TxOut out; + out.value = value; + tx.vout.push_back(out); + return tx; +} + +MutableTransaction make_gentx() +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0xffffffff; // coinbase + in.sequence = 0xffffffff; + tx.vin.push_back(in); + TxOut out; + out.value = 5000000000LL; + tx.vout.push_back(out); + return tx; +} + +std::vector noseg_bytes(const MutableTransaction& tx) +{ + auto packed = pack(TX_NO_WITNESS(tx)); + auto sp = packed.get_span(); + return std::vector( + reinterpret_cast(sp.data()), + reinterpret_cast(sp.data()) + sp.size()); +} + +std::string withwit_hex(const MutableTransaction& tx) +{ + return HexStr(pack(TX_WITH_WITNESS(tx)).get_span()); +} + +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 won_hash() +{ + uint256 h; h.SetHex("00000000000000000000000000000000000000000000000000000000000000a0"); + return h; +} + +} // namespace + +// --- Test 1: round-trip -- the production template txs decode byte-faithfully - +// Populate a Mempool, shape it through the PRODUCTION make_mempool_tx_source, +// then decode the resulting transactions[] back through the bridge. Each decoded +// tx must re-serialize (with-witness) to the exact `data` the template carried +// and carry the exact `txid`, in template order. +TEST(DgbTemplateOtherTxs, RoundTripsProductionTemplateTxs) +{ + Mempool pool; + MutableTransaction a = tagged_tx(10, 0); + MutableTransaction b = tagged_tx(20, 1); + ASSERT_TRUE(pool.add_tx(a)); + ASSERT_TRUE(pool.add_tx(b)); + pool.set_tx_fee(compute_txid(a), 700); + pool.set_tx_fee(compute_txid(b), 300); + + auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000); + const auto sel = source(); + ASSERT_EQ(sel.transactions.size(), 2u); + + const auto txs = deserialize_template_other_txs(sel.transactions); + ASSERT_EQ(txs.size(), sel.transactions.size()); // template order preserved + + for (size_t i = 0; i < txs.size(); ++i) + { + // Decoded tx re-serializes to the exact template `data` and `txid`. + EXPECT_EQ(withwit_hex(txs[i]), sel.transactions[i]["data"].get()); + EXPECT_EQ(compute_txid(txs[i]).GetHex(), sel.transactions[i]["txid"].get()); + } +} + +// --- Test 2: empty / absent transactions[] -> empty vector (coinbase-only) ---- +TEST(DgbTemplateOtherTxs, EmptyTransactionsIsEmptyVector) +{ + EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json::array()).empty()); + EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json(nullptr)).empty()); +} + +// --- Test 3: malformed `data` (trailing byte) -> throws (fail-closed) --------- +TEST(DgbTemplateOtherTxs, TrailingBytesThrow) +{ + std::string good = withwit_hex(tagged_tx(10, 0)); + EXPECT_NO_THROW(deserialize_template_tx(good)); + EXPECT_THROW(deserialize_template_tx(good + "ff"), std::out_of_range); // junk byte +} + +// --- Test 4: end-to-end -- funded template txs land in the reconstructed block - +// The bridge, wired as the reconstructor's template_other_txs_fn via a per-share +// captured-transactions[] provider, reconstructs the SAME block that feeding the +// deserialized txs straight into reconstruct_won_block_from_template produces: +// [gentx] ++ the mempool-funded template txs, byte-identical. +TEST(DgbTemplateOtherTxs, ReconstructsBlockWithFundedTemplateTxs) +{ + Mempool pool; + MutableTransaction a = tagged_tx(10, 0); + MutableTransaction b = tagged_tx(20, 1); + ASSERT_TRUE(pool.add_tx(a)); + ASSERT_TRUE(pool.add_tx(b)); + pool.set_tx_fee(compute_txid(a), 700); + pool.set_tx_fee(compute_txid(b), 300); + const auto sel = make_mempool_tx_source(pool, /*max_weight=*/4'000'000)(); + ASSERT_FALSE(sel.transactions.empty()); + + auto sh = make_small_header(); + ::dgb::MerkleLink link; + const uint256 won = won_hash(); + auto gentx_bytes = noseg_bytes(make_gentx()); + auto ug = unpack_gentx_coinbase(gentx_bytes); + + // Expected: [gentx] ++ decoded-template-txs straight through the SSOT. + const auto other = deserialize_template_other_txs(sel.transactions); + const auto expected = + reconstruct_won_block_from_template(sh, link, ug.tx, ug.txid, other); + + // Actual: the run-loop shape -- closure pulls the per-share captured txs[] + // through the bridge factory. + auto txs_json = sel.transactions; + auto closure = make_reconstruct_closure_from_template( + [won, sh, link](const uint256& h) -> WonShareInputs { + if (h != won) throw std::out_of_range("unknown share"); + return WonShareInputs{sh, link}; + }, + [gentx_bytes](const uint256&) { return gentx_bytes; }, + make_template_other_txs_fn( + [txs_json](const uint256&) { return txs_json; })); + + auto got = closure(won); + ASSERT_TRUE(got.has_value()); + EXPECT_FALSE(got->first.empty()); + EXPECT_EQ(got->first, expected.bytes); // funded txs land in the block + EXPECT_EQ(got->second, expected.hex); +} + +// --- Test 5: a bad captured provider fails the whole won block CLOSED ---------- +TEST(DgbTemplateOtherTxs, BadProviderFailsClosed) +{ + auto sh = make_small_header(); + ::dgb::MerkleLink link; + const uint256 won = won_hash(); + auto gentx_bytes = noseg_bytes(make_gentx()); + + nlohmann::json bad = nlohmann::json::array(); + bad.push_back({{"data", withwit_hex(tagged_tx(10, 0)) + "ff"}}); // trailing junk + + auto closure = make_reconstruct_closure_from_template( + [won, sh, link](const uint256& h) -> WonShareInputs { + if (h != won) throw std::out_of_range("unknown share"); + return WonShareInputs{sh, link}; + }, + [gentx_bytes](const uint256&) { return gentx_bytes; }, + make_template_other_txs_fn([bad](const uint256&) { return bad; })); + + EXPECT_FALSE(closure(won).has_value()); // decode throw -> nullopt, no broadcast +}