Skip to content

Commit f980a99

Browse files
committed
btc(coin): template_other_txs decode wire — captured GBT template -> reconstructor other_txs (reconstructor slice 6/7)
btc::coin::template_other_txs bridges TemplateCapture transactions[] to the won-block reconstructor template_other_txs_fn seam: deserialize_template_tx / deserialize_template_other_txs (template order; empty-array => coinbase-only; malformed data => throw => fail CLOSED) + make_template_other_txs_fn adapting the captured-tx provider. Decode wire only; main_btc run-loop wiring is slice 7. btc_share_test 79/79 (BtcTemplateOtherTxs 4/4), btc-tree-only.
1 parent fb89281 commit f980a99

3 files changed

Lines changed: 277 additions & 1 deletion

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
#pragma once
3+
// ---------------------------------------------------------------------------
4+
// btc::coin::template_other_txs -- the producer bridge between the captured GBT
5+
// work template's transactions[] (TemplateCapture, reconstructor slice 3 / #837)
6+
// and the won-block reconstructor's template_other_txs_fn seam
7+
// (reconstruct_won_block.hpp, make_reconstruct_closure -- slice 5 / #839).
8+
//
9+
// The reconstructor frames the broadcast block as [gentx] ++ other_txs, where
10+
// other_txs is the captured template's non-coinbase set in template order
11+
// (block tx order after the coinbase). The captured template carries each tx as
12+
// a conformant GBT entry {data,txid,hash,fee} (exactly bitcoind's
13+
// getblocktemplate shape); the reconstructor consumes them as already-
14+
// deserialized btc::coin::MutableTransaction. This header is the missing decode
15+
// step: the GBT `data` (with-witness) hex -> MutableTransaction, in template
16+
// order. It is the literal wire between TemplateCapture (the template's tx
17+
// SOURCE) and the reconstruct closure (the broadcast tx SINK).
18+
//
19+
// A won BTC share commits to the template it was mined against, so the
20+
// broadcast block's non-coinbase set MUST be that template's transactions[] --
21+
// NOT the live mempool selection, and NOT the share's transaction_hash_refs
22+
// (v34+ SegwitMining/PaddingBugfix/MergedMining shares carry no m_tx_info, so
23+
// the share never carried the block tx set for ANY current version). That is
24+
// why the reconstructor is template-sourced.
25+
//
26+
// SSOT split:
27+
// * deserialize_template_tx(data_hex) -- pure: one GBT `data` -> tx.
28+
// * deserialize_template_other_txs(json) -- pure: transactions[] -> txs[],
29+
// in template order. An empty / absent array yields an empty vector (a
30+
// valid coinbase-only block, the reconstructor's documented empty contract).
31+
// Throws on a malformed `data` entry (bad hex / trailing bytes) so the
32+
// closure's broad catch fails the whole won block CLOSED rather than
33+
// broadcasting a half-decoded tx set (mirrors unpack_gentx_coinbase).
34+
// * make_template_other_txs_fn(captured_template_txs_fn) -- adapts the per-
35+
// share captured-transactions[] provider (TemplateCapture::provider(), #837)
36+
// into the template_other_txs_fn signature make_reconstruct_closure
37+
// installs. Any decode error propagates so the reconstruct closure fails
38+
// the won block CLOSED. Wiring the concrete provider is the main_btc run-loop
39+
// integration (slice 7); this header owns ONLY the decode wire.
40+
//
41+
// Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE -- the
42+
// transactions[] form is already the conformant GBT shape; this only decodes it
43+
// back into the in-memory tx the block carries. BTC is a standalone SHA256d
44+
// parent (no merged-coinbase leg).
45+
// ---------------------------------------------------------------------------
46+
47+
#include <functional>
48+
#include <stdexcept>
49+
#include <string>
50+
#include <utility>
51+
#include <vector>
52+
53+
#include <nlohmann/json.hpp>
54+
55+
#include <core/pack.hpp>
56+
#include <core/uint256.hpp>
57+
#include <btclibs/util/strencodings.h> // ParseHex
58+
59+
#include "transaction.hpp" // MutableTransaction, UnserializeTransaction, TX_WITH_WITNESS, PackStream
60+
61+
namespace btc
62+
{
63+
namespace coin
64+
{
65+
66+
// Decode one GBT `data` (with-witness) hex string into a MutableTransaction.
67+
// The captured template's `data` is the full serialized transaction WITH
68+
// witness (bitcoind getblocktemplate), so decode symmetrically with
69+
// TX_WITH_WITNESS. Throws std::out_of_range if the hex carries trailing bytes
70+
// past a complete transaction (a malformed template entry), matching
71+
// unpack_gentx_coinbase's fail-closed contract for the gentx.
72+
inline MutableTransaction
73+
deserialize_template_tx(const std::string& data_hex)
74+
{
75+
PackStream ps(ParseHex(data_hex));
76+
MutableTransaction tx;
77+
UnserializeTransaction(tx, ps, TX_WITH_WITNESS);
78+
if (!ps.empty())
79+
throw std::out_of_range(
80+
"deserialize_template_tx: trailing bytes after tx -- "
81+
"malformed GBT template `data` entry");
82+
return tx;
83+
}
84+
85+
// Decode the captured GBT template's transactions[] into the won-block other-tx
86+
// vector, in template order (block tx order after the coinbase). Each entry's
87+
// `data` field is the with-witness bytes the template carried. An empty /
88+
// absent array yields an empty vector (a valid coinbase-only block).
89+
inline std::vector<MutableTransaction>
90+
deserialize_template_other_txs(const nlohmann::json& transactions)
91+
{
92+
std::vector<MutableTransaction> out;
93+
if (transactions.is_array())
94+
{
95+
out.reserve(transactions.size());
96+
for (const auto& entry : transactions)
97+
out.push_back(deserialize_template_tx(entry.at("data").get<std::string>()));
98+
}
99+
return out;
100+
}
101+
102+
// Adapt a per-share captured-transactions[] provider into the run-loop's
103+
// template_other_txs_fn (the third argument of make_reconstruct_closure).
104+
// captured_template_txs_fn(share_hash) MUST return the transactions[] of the
105+
// template the won share was mined against (per-job capture, #837); decoded here
106+
// through the deserialize SSOT. Any decode error propagates so the reconstruct
107+
// closure fails the won block CLOSED.
108+
inline std::function<std::vector<MutableTransaction>(const uint256&)>
109+
make_template_other_txs_fn(
110+
std::function<nlohmann::json(const uint256&)> captured_template_txs_fn)
111+
{
112+
return [captured_template_txs_fn = std::move(captured_template_txs_fn)](
113+
const uint256& share_hash) -> std::vector<MutableTransaction> {
114+
return deserialize_template_other_txs(captured_template_txs_fn(share_hash));
115+
};
116+
}
117+
118+
} // namespace coin
119+
} // namespace btc

src/impl/btc/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
if (BUILD_TESTING AND GTest_FOUND)
22
# btc twin of ltc share_test — uniquely named to avoid the CMP0002 target
33
# collision with src/impl/ltc/test (both subdirs build in the same tree).
4-
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 gentx_coinbase_test.cpp)
4+
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 gentx_coinbase_test.cpp template_other_txs_test.cpp)
55
target_link_libraries(btc_share_test PRIVATE
66
GTest::gtest_main GTest::gtest
77
core btc
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// ---------------------------------------------------------------------------
3+
// btc_template_other_txs (rides btc_share_test) -- pins
4+
// coin/template_other_txs.hpp, the producer bridge that decodes the captured
5+
// GBT work template's transactions[] (the conformant {data,txid,hash,fee} shape
6+
// bitcoind's getblocktemplate emits, retained per-job by TemplateCapture / #837)
7+
// back into the MutableTransaction vector the won-block reconstructor's
8+
// template_other_txs_fn seam frames as [gentx] ++ other_txs (#839).
9+
//
10+
// This closes the decode half of the loop: the SAME txs the captured template
11+
// carried are the txs that land in the reconstructed broadcast block, byte-
12+
// faithfully and in template order. The header depends ONLY on transaction.hpp,
13+
// so this KAT stands alone off master (no reconstruct-closure / TemplateCapture
14+
// link -- those slices land separately; the run-loop composition is slice 7).
15+
//
16+
// Rides the already-allowlisted btc_share_test executable, so no build.yml
17+
// --target change is needed (#143 NOT_BUILT trap). p2pool-merged-v36 surface:
18+
// NONE. Per-coin isolation: src/impl/btc/ only.
19+
// ---------------------------------------------------------------------------
20+
21+
#include <gtest/gtest.h>
22+
23+
#include <cstdint>
24+
#include <functional>
25+
#include <stdexcept>
26+
#include <string>
27+
#include <vector>
28+
29+
#include <nlohmann/json.hpp>
30+
31+
#include <core/pack.hpp>
32+
#include <core/hash.hpp>
33+
#include <core/uint256.hpp>
34+
#include <btclibs/util/strencodings.h>
35+
36+
#include <impl/btc/coin/template_other_txs.hpp>
37+
#include <impl/btc/coin/transaction.hpp>
38+
39+
namespace {
40+
41+
using btc::coin::MutableTransaction;
42+
using btc::coin::TxIn;
43+
using btc::coin::TxOut;
44+
using btc::coin::TX_WITH_WITNESS;
45+
using btc::coin::TX_NO_WITNESS;
46+
using btc::coin::deserialize_template_tx;
47+
using btc::coin::deserialize_template_other_txs;
48+
using btc::coin::make_template_other_txs_fn;
49+
50+
// A minimal witnessless payment tx tagged by output value + prevout index, so
51+
// two entries in one template are distinguishable after the round trip.
52+
MutableTransaction tagged_tx(int64_t value, uint32_t index)
53+
{
54+
MutableTransaction tx;
55+
tx.version = 1;
56+
tx.locktime = 0;
57+
TxIn in;
58+
in.prevout.hash.SetNull();
59+
in.prevout.index = index;
60+
in.sequence = 0xffffffff;
61+
tx.vin.push_back(in);
62+
TxOut out;
63+
out.value = value;
64+
tx.vout.push_back(out);
65+
return tx;
66+
}
67+
68+
// The with-witness `data` hex the GBT template carries (== non-witness bytes for
69+
// a witnessless tx: no segwit marker is emitted when HasWitness()==false).
70+
// Single-expression: the temporary PackStream lives to the end of the full
71+
// expression, so get_span() does not dangle (cf. reconstruct_test.cpp note).
72+
std::string withwit_hex(const MutableTransaction& tx)
73+
{
74+
return HexStr(pack(TX_WITH_WITNESS(tx)).get_span());
75+
}
76+
77+
uint256 txid(const MutableTransaction& tx)
78+
{
79+
return Hash(pack(TX_NO_WITNESS(tx)).get_span());
80+
}
81+
82+
// Emulate the captured GBT transactions[]: an ordered array of {data,txid}.
83+
nlohmann::json gbt_entry(const MutableTransaction& tx)
84+
{
85+
nlohmann::json e;
86+
e["data"] = withwit_hex(tx);
87+
e["txid"] = txid(tx).GetHex();
88+
return e;
89+
}
90+
91+
} // namespace
92+
93+
// --- Test 1: round-trip -- template txs decode byte-faithfully, in order ------
94+
// Each decoded tx must re-serialize (with-witness) to the exact `data` the
95+
// template carried and carry the exact txid, in template order.
96+
TEST(BtcTemplateOtherTxs, RoundTripsTemplateTxs)
97+
{
98+
MutableTransaction a = tagged_tx(10, 0);
99+
MutableTransaction b = tagged_tx(20, 1);
100+
nlohmann::json transactions = nlohmann::json::array({gbt_entry(a), gbt_entry(b)});
101+
102+
const auto txs = deserialize_template_other_txs(transactions);
103+
ASSERT_EQ(txs.size(), 2u); // template order preserved
104+
105+
EXPECT_EQ(withwit_hex(txs[0]), transactions[0]["data"].get<std::string>());
106+
EXPECT_EQ(txid(txs[0]).GetHex(), transactions[0]["txid"].get<std::string>());
107+
EXPECT_EQ(withwit_hex(txs[1]), transactions[1]["data"].get<std::string>());
108+
EXPECT_EQ(txid(txs[1]).GetHex(), transactions[1]["txid"].get<std::string>());
109+
// Distinguishable -> order really is [a, b], not swapped.
110+
EXPECT_EQ(txs[0].vout[0].value, 10);
111+
EXPECT_EQ(txs[1].vout[0].value, 20);
112+
}
113+
114+
// --- Test 2: empty / absent transactions[] -> empty vector (coinbase-only) ----
115+
TEST(BtcTemplateOtherTxs, EmptyTransactionsIsEmptyVector)
116+
{
117+
EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json::array()).empty());
118+
EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json(nullptr)).empty());
119+
}
120+
121+
// --- Test 3: malformed `data` (trailing byte) -> throws (fail-closed) ---------
122+
TEST(BtcTemplateOtherTxs, TrailingBytesThrow)
123+
{
124+
const std::string good = withwit_hex(tagged_tx(10, 0));
125+
EXPECT_NO_THROW(deserialize_template_tx(good));
126+
EXPECT_THROW(deserialize_template_tx(good + "ff"), std::out_of_range); // junk byte
127+
// ... and it fails the whole array closed, not just the bad entry.
128+
nlohmann::json transactions = nlohmann::json::array();
129+
transactions.push_back({{"data", good + "ff"}});
130+
EXPECT_THROW(deserialize_template_other_txs(transactions), std::out_of_range);
131+
}
132+
133+
// --- Test 4: provider factory -- the run-loop wire shape ----------------------
134+
// make_template_other_txs_fn wraps a per-share captured-transactions[] provider
135+
// (TemplateCapture::provider() shape) into the template_other_txs_fn seam. A hit
136+
// decodes the retained template; a miss (empty array, TemplateCapture's
137+
// documented miss policy) yields a coinbase-only empty vector.
138+
TEST(BtcTemplateOtherTxs, ProviderFactoryDecodesCapturedTemplate)
139+
{
140+
MutableTransaction a = tagged_tx(30, 0);
141+
nlohmann::json captured = nlohmann::json::array({gbt_entry(a)});
142+
143+
uint256 won; won.SetHex("00000000000000000000000000000000000000000000000000000000000000a0");
144+
uint256 other; other.SetHex("00000000000000000000000000000000000000000000000000000000000000b0");
145+
146+
auto fn = make_template_other_txs_fn(
147+
[won, captured](const uint256& h) -> nlohmann::json {
148+
return h == won ? captured : nlohmann::json::array(); // miss -> empty
149+
});
150+
151+
const auto hit = fn(won);
152+
ASSERT_EQ(hit.size(), 1u);
153+
EXPECT_EQ(hit[0].vout[0].value, 30);
154+
EXPECT_EQ(withwit_hex(hit[0]), captured[0]["data"].get<std::string>());
155+
156+
EXPECT_TRUE(fn(other).empty()); // coinbase-only on capture miss
157+
}

0 commit comments

Comments
 (0)