diff --git a/src/impl/btc/coin/template_other_txs.hpp b/src/impl/btc/coin/template_other_txs.hpp new file mode 100644 index 000000000..3a7878d3d --- /dev/null +++ b/src/impl/btc/coin/template_other_txs.hpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once +// --------------------------------------------------------------------------- +// btc::coin::template_other_txs -- the producer bridge between the captured GBT +// work template's transactions[] (TemplateCapture, reconstructor slice 3 / #837) +// and the won-block reconstructor's template_other_txs_fn seam +// (reconstruct_won_block.hpp, make_reconstruct_closure -- slice 5 / #839). +// +// The reconstructor frames the broadcast block as [gentx] ++ other_txs, where +// other_txs is the captured template's non-coinbase set in template order +// (block tx order after the coinbase). The captured template carries each tx as +// a conformant GBT entry {data,txid,hash,fee} (exactly bitcoind's +// getblocktemplate shape); the reconstructor consumes them as already- +// deserialized btc::coin::MutableTransaction. This header is the missing decode +// step: the GBT `data` (with-witness) hex -> MutableTransaction, in template +// order. It is the literal wire between TemplateCapture (the template's tx +// SOURCE) and the reconstruct closure (the broadcast tx SINK). +// +// A won BTC share commits to the template it was mined against, so the +// broadcast block's non-coinbase set MUST be that template's transactions[] -- +// NOT the live mempool selection, and NOT the share's transaction_hash_refs +// (v34+ SegwitMining/PaddingBugfix/MergedMining shares carry no m_tx_info, so +// the share never carried the block tx set for ANY current version). That is +// why the reconstructor is template-sourced. +// +// SSOT split: +// * deserialize_template_tx(data_hex) -- pure: one GBT `data` -> tx. +// * deserialize_template_other_txs(json) -- pure: transactions[] -> txs[], +// in template order. An empty / absent array yields an empty vector (a +// valid coinbase-only block, the reconstructor's documented empty contract). +// 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 the per- +// share captured-transactions[] provider (TemplateCapture::provider(), #837) +// into the template_other_txs_fn signature make_reconstruct_closure +// installs. Any decode error propagates so the reconstruct closure fails +// the won block CLOSED. Wiring the concrete provider is the main_btc run-loop +// integration (slice 7); this header owns ONLY the decode wire. +// +// Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE -- the +// transactions[] form is already the conformant GBT shape; this only decodes it +// back into the in-memory tx the block carries. BTC is a standalone SHA256d +// parent (no merged-coinbase leg). +// --------------------------------------------------------------------------- + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include // ParseHex + +#include "transaction.hpp" // MutableTransaction, UnserializeTransaction, TX_WITH_WITNESS, PackStream + +namespace btc +{ +namespace coin +{ + +// Decode one GBT `data` (with-witness) hex string into a MutableTransaction. +// The captured template's `data` is the full serialized transaction WITH +// witness (bitcoind getblocktemplate), so decode symmetrically with +// TX_WITH_WITNESS. 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; + 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 the template carried. 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). +// captured_template_txs_fn(share_hash) MUST return the transactions[] of the +// template the won share was mined against (per-job capture, #837); 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 btc diff --git a/src/impl/btc/test/CMakeLists.txt b/src/impl/btc/test/CMakeLists.txt index 8cf681a4f..aa0358a6c 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) + 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) target_link_libraries(btc_share_test PRIVATE GTest::gtest_main GTest::gtest core btc diff --git a/src/impl/btc/test/template_other_txs_test.cpp b/src/impl/btc/test/template_other_txs_test.cpp new file mode 100644 index 000000000..c031b50a4 --- /dev/null +++ b/src/impl/btc/test/template_other_txs_test.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// --------------------------------------------------------------------------- +// btc_template_other_txs (rides btc_share_test) -- pins +// coin/template_other_txs.hpp, the producer bridge that decodes the captured +// GBT work template's transactions[] (the conformant {data,txid,hash,fee} shape +// bitcoind's getblocktemplate emits, retained per-job by TemplateCapture / #837) +// back into the MutableTransaction vector the won-block reconstructor's +// template_other_txs_fn seam frames as [gentx] ++ other_txs (#839). +// +// This closes the decode half of the loop: the SAME txs the captured template +// carried are the txs that land in the reconstructed broadcast block, byte- +// faithfully and in template order. The header depends ONLY on transaction.hpp, +// so this KAT stands alone off master (no reconstruct-closure / TemplateCapture +// link -- those slices land separately; the run-loop composition is slice 7). +// +// Rides the already-allowlisted btc_share_test executable, so no build.yml +// --target change is needed (#143 NOT_BUILT trap). p2pool-merged-v36 surface: +// NONE. Per-coin isolation: src/impl/btc/ only. +// --------------------------------------------------------------------------- + +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +namespace { + +using btc::coin::MutableTransaction; +using btc::coin::TxIn; +using btc::coin::TxOut; +using btc::coin::TX_WITH_WITNESS; +using btc::coin::TX_NO_WITNESS; +using btc::coin::deserialize_template_tx; +using btc::coin::deserialize_template_other_txs; +using btc::coin::make_template_other_txs_fn; + +// A minimal witnessless payment tx tagged by output value + prevout index, so +// two entries in one template are distinguishable after the round trip. +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; +} + +// The with-witness `data` hex the GBT template carries (== non-witness bytes for +// a witnessless tx: no segwit marker is emitted when HasWitness()==false). +// Single-expression: the temporary PackStream lives to the end of the full +// expression, so get_span() does not dangle (cf. reconstruct_test.cpp note). +std::string withwit_hex(const MutableTransaction& tx) +{ + return HexStr(pack(TX_WITH_WITNESS(tx)).get_span()); +} + +uint256 txid(const MutableTransaction& tx) +{ + return Hash(pack(TX_NO_WITNESS(tx)).get_span()); +} + +// Emulate the captured GBT transactions[]: an ordered array of {data,txid}. +nlohmann::json gbt_entry(const MutableTransaction& tx) +{ + nlohmann::json e; + e["data"] = withwit_hex(tx); + e["txid"] = txid(tx).GetHex(); + return e; +} + +} // namespace + +// --- Test 1: round-trip -- template txs decode byte-faithfully, in order ------ +// Each decoded tx must re-serialize (with-witness) to the exact `data` the +// template carried and carry the exact txid, in template order. +TEST(BtcTemplateOtherTxs, RoundTripsTemplateTxs) +{ + MutableTransaction a = tagged_tx(10, 0); + MutableTransaction b = tagged_tx(20, 1); + nlohmann::json transactions = nlohmann::json::array({gbt_entry(a), gbt_entry(b)}); + + const auto txs = deserialize_template_other_txs(transactions); + ASSERT_EQ(txs.size(), 2u); // template order preserved + + EXPECT_EQ(withwit_hex(txs[0]), transactions[0]["data"].get()); + EXPECT_EQ(txid(txs[0]).GetHex(), transactions[0]["txid"].get()); + EXPECT_EQ(withwit_hex(txs[1]), transactions[1]["data"].get()); + EXPECT_EQ(txid(txs[1]).GetHex(), transactions[1]["txid"].get()); + // Distinguishable -> order really is [a, b], not swapped. + EXPECT_EQ(txs[0].vout[0].value, 10); + EXPECT_EQ(txs[1].vout[0].value, 20); +} + +// --- Test 2: empty / absent transactions[] -> empty vector (coinbase-only) ---- +TEST(BtcTemplateOtherTxs, 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(BtcTemplateOtherTxs, TrailingBytesThrow) +{ + const 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 + // ... and it fails the whole array closed, not just the bad entry. + nlohmann::json transactions = nlohmann::json::array(); + transactions.push_back({{"data", good + "ff"}}); + EXPECT_THROW(deserialize_template_other_txs(transactions), std::out_of_range); +} + +// --- Test 4: provider factory -- the run-loop wire shape ---------------------- +// make_template_other_txs_fn wraps a per-share captured-transactions[] provider +// (TemplateCapture::provider() shape) into the template_other_txs_fn seam. A hit +// decodes the retained template; a miss (empty array, TemplateCapture's +// documented miss policy) yields a coinbase-only empty vector. +TEST(BtcTemplateOtherTxs, ProviderFactoryDecodesCapturedTemplate) +{ + MutableTransaction a = tagged_tx(30, 0); + nlohmann::json captured = nlohmann::json::array({gbt_entry(a)}); + + uint256 won; won.SetHex("00000000000000000000000000000000000000000000000000000000000000a0"); + uint256 other; other.SetHex("00000000000000000000000000000000000000000000000000000000000000b0"); + + auto fn = make_template_other_txs_fn( + [won, captured](const uint256& h) -> nlohmann::json { + return h == won ? captured : nlohmann::json::array(); // miss -> empty + }); + + const auto hit = fn(won); + ASSERT_EQ(hit.size(), 1u); + EXPECT_EQ(hit[0].vout[0].value, 30); + EXPECT_EQ(withwit_hex(hit[0]), captured[0]["data"].get()); + + EXPECT_TRUE(fn(other).empty()); // coinbase-only on capture miss +}