Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions src/impl/btc/coin/template_other_txs.hpp
Original file line number Diff line number Diff line change
@@ -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 <functional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

#include <nlohmann/json.hpp>

#include <core/pack.hpp>
#include <core/uint256.hpp>
#include <btclibs/util/strencodings.h> // 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<MutableTransaction>
deserialize_template_other_txs(const nlohmann::json& transactions)
{
std::vector<MutableTransaction> out;
if (transactions.is_array())
{
out.reserve(transactions.size());
for (const auto& entry : transactions)
out.push_back(deserialize_template_tx(entry.at("data").get<std::string>()));
}
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<std::vector<MutableTransaction>(const uint256&)>
make_template_other_txs_fn(
std::function<nlohmann::json(const uint256&)> captured_template_txs_fn)
{
return [captured_template_txs_fn = std::move(captured_template_txs_fn)](
const uint256& share_hash) -> std::vector<MutableTransaction> {
return deserialize_template_other_txs(captured_template_txs_fn(share_hash));
};
}

} // namespace coin
} // namespace btc
2 changes: 1 addition & 1 deletion src/impl/btc/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
157 changes: 157 additions & 0 deletions src/impl/btc/test/template_other_txs_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>

#include <cstdint>
#include <functional>
#include <stdexcept>
#include <string>
#include <vector>

#include <nlohmann/json.hpp>

#include <core/pack.hpp>
#include <core/hash.hpp>
#include <core/uint256.hpp>
#include <btclibs/util/strencodings.h>

#include <impl/btc/coin/template_other_txs.hpp>
#include <impl/btc/coin/transaction.hpp>

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<std::string>());
EXPECT_EQ(txid(txs[0]).GetHex(), transactions[0]["txid"].get<std::string>());
EXPECT_EQ(withwit_hex(txs[1]), transactions[1]["data"].get<std::string>());
EXPECT_EQ(txid(txs[1]).GetHex(), transactions[1]["txid"].get<std::string>());
// 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<std::string>());

EXPECT_TRUE(fn(other).empty()); // coinbase-only on capture miss
}
Loading