Skip to content

Commit c13ab2f

Browse files
authored
Merge pull request #299 from frstrtr/dgb/tx-inclusion-template-bridge
dgb: template-txs producer bridge (tx-inclusion slice — decode GBT transactions[] into won-block other_txs)
2 parents d6fcd93 + bf74290 commit c13ab2f

4 files changed

Lines changed: 357 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ jobs:
8282
test_address_resolution test_compute_share_target \
8383
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 \
8484
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 \
85-
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 \
85+
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 \
8686
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
8787
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 \
8888
-j$(nproc)
@@ -214,7 +214,7 @@ jobs:
214214
test_address_resolution test_compute_share_target \
215215
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 \
216216
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 \
217-
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 \
217+
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 \
218218
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
219219
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 \
220220
v37_test \
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// ---------------------------------------------------------------------------
2+
// template_other_txs.hpp -- the producer bridge between the embedded work
3+
// template's transactions[] (make_mempool_tx_source / embedded_tx_select.cpp)
4+
// and the won-block reconstructor's template_other_txs_fn seam
5+
// (reconstruct_closure.hpp, make_reconstruct_closure_from_template).
6+
//
7+
// The reconstructor frames the broadcast block as [gentx] ++ other_txs, where
8+
// other_txs is the captured-GBT template's non-coinbase set in template order
9+
// (#271). The template carries each tx as a GBT entry {data,txid,hash,fee}; the
10+
// reconstructor consumes them as already-deserialized dgb::coin::Mutable-
11+
// Transaction. This header is the missing deserialize step: the GBT `data`
12+
// (with-witness) hex -> MutableTransaction, in template order. It is the literal
13+
// wire between make_mempool_tx_source (the template's tx SOURCE) and the
14+
// reconstructor (the broadcast tx SINK).
15+
//
16+
// SSOT split:
17+
// * deserialize_template_other_txs(json) -- pure: transactions[] -> txs[].
18+
// Throws on a malformed `data` entry (bad hex / trailing bytes) so the
19+
// closure's broad catch fails the whole won block CLOSED rather than
20+
// broadcasting a half-decoded tx set (mirrors unpack_gentx_coinbase).
21+
// * make_template_other_txs_fn(captured_template_txs_fn) -- adapts a per-share
22+
// captured-transactions[] provider into the template_other_txs_fn signature
23+
// make_reconstruct_closure_from_template installs. The provider MUST return
24+
// the transactions[] of the GBT TEMPLATE the won share was handed at job
25+
// hand-out (the per-job capture seam, #271), NOT the live mempool selection:
26+
// a won share commits to the template it was mined against, which may differ
27+
// from the current mempool. Wiring that per-job capture provider is the
28+
// run-loop integration; this header owns ONLY the deserialize wire.
29+
//
30+
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE -- the
31+
// transactions[] form is already the conformant GBT shape make_mempool_tx_source
32+
// emits; this only decodes it back into the in-memory tx the block carries.
33+
// ---------------------------------------------------------------------------
34+
#pragma once
35+
36+
#include <functional>
37+
#include <stdexcept>
38+
#include <string>
39+
#include <utility>
40+
#include <vector>
41+
42+
#include <nlohmann/json.hpp>
43+
44+
#include <core/pack.hpp>
45+
#include <core/uint256.hpp>
46+
#include <btclibs/util/strencodings.h> // ParseHex
47+
48+
#include "transaction.hpp" // MutableTransaction, UnserializeTransaction, TX_WITH_WITNESS
49+
50+
namespace dgb
51+
{
52+
namespace coin
53+
{
54+
55+
// Decode one GBT `data` (with-witness) hex string into a MutableTransaction.
56+
// Throws std::out_of_range if the hex carries trailing bytes past a complete
57+
// transaction (a malformed template entry), matching unpack_gentx_coinbase's
58+
// fail-closed contract for the gentx.
59+
inline MutableTransaction
60+
deserialize_template_tx(const std::string& data_hex)
61+
{
62+
PackStream ps(ParseHex(data_hex));
63+
MutableTransaction tx;
64+
// With-witness parse: make_mempool_tx_source emits `data` as the with-
65+
// witness submit bytes (pack(TX_WITH_WITNESS(tx))), so decode symmetrically.
66+
UnserializeTransaction(tx, ps, TX_WITH_WITNESS);
67+
if (!ps.empty())
68+
throw std::out_of_range(
69+
"deserialize_template_tx: trailing bytes after tx -- "
70+
"malformed GBT template `data` entry");
71+
return tx;
72+
}
73+
74+
// Decode the captured GBT template's transactions[] into the won-block other-tx
75+
// vector, in template order (block tx order after the coinbase). Each entry's
76+
// `data` field is the with-witness bytes make_mempool_tx_source emitted. An
77+
// empty / absent array yields an empty vector (a valid coinbase-only block).
78+
inline std::vector<MutableTransaction>
79+
deserialize_template_other_txs(const nlohmann::json& transactions)
80+
{
81+
std::vector<MutableTransaction> out;
82+
if (transactions.is_array())
83+
{
84+
out.reserve(transactions.size());
85+
for (const auto& entry : transactions)
86+
out.push_back(deserialize_template_tx(entry.at("data").get<std::string>()));
87+
}
88+
return out;
89+
}
90+
91+
// Adapt a per-share captured-transactions[] provider into the run-loop's
92+
// template_other_txs_fn (the third argument of make_reconstruct_closure_from_
93+
// template). captured_template_txs_fn(share_hash) MUST return the transactions[]
94+
// of the template the won share was mined against (per-job capture, #271);
95+
// decoded here through the deserialize SSOT. Any decode error propagates so the
96+
// reconstruct closure fails the won block CLOSED.
97+
inline std::function<std::vector<MutableTransaction>(const uint256&)>
98+
make_template_other_txs_fn(
99+
std::function<nlohmann::json(const uint256&)> captured_template_txs_fn)
100+
{
101+
return [captured_template_txs_fn = std::move(captured_template_txs_fn)](
102+
const uint256& share_hash) -> std::vector<MutableTransaction> {
103+
return deserialize_template_other_txs(captured_template_txs_fn(share_hash));
104+
};
105+
}
106+
107+
} // namespace coin
108+
} // namespace dgb

src/impl/dgb/test/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,21 @@ if (BUILD_TESTING AND GTest_FOUND)
259259
nlohmann_json::nlohmann_json)
260260
gtest_add_tests(dgb_embedded_tx_select_test "" AUTO)
261261

262+
# dgb_template_other_txs_test: pins coin/template_other_txs.hpp, the producer
263+
# bridge decoding the embedded template transactions[] (make_mempool_tx_source
264+
# GBT data) back into the MutableTransaction vector the won-block reconstructor
265+
# frames as [gentx] ++ other_txs. Compiles the tx codec AND the reconstruct
266+
# closure, so it links the embedded_tx_select SCC set + pool/sharechain. MUST
267+
# also be in BOTH build.yml --target allowlists (#143 NOT_BUILT trap).
268+
add_executable(dgb_template_other_txs_test template_other_txs_test.cpp)
269+
target_link_libraries(dgb_template_other_txs_test PRIVATE
270+
GTest::gtest_main GTest::gtest
271+
core dgb dgb_stratum
272+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
273+
dgb_coin pool sharechain
274+
nlohmann_json::nlohmann_json)
275+
gtest_add_tests(dgb_template_other_txs_test "" AUTO)
276+
262277
# dgb_coinbase_value_parity_test: END-TO-END GBT-parity KAT. Drives a
263278
# populated Mempool through the production make_mempool_tx_source ->
264279
# EmbeddedCoinNode -> build_work_template path and asserts coinbasevalue ==
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// ---------------------------------------------------------------------------
2+
// dgb_template_other_txs_test -- pins coin/template_other_txs.hpp, the producer
3+
// bridge that decodes the embedded work template's transactions[] (the GBT data
4+
// the miner is funded with, shaped by make_mempool_tx_source) back into the
5+
// MutableTransaction vector the won-block reconstructor's template_other_txs_fn
6+
// seam frames as [gentx] ++ other_txs.
7+
//
8+
// This closes the loop between the two already-pinned halves:
9+
// * make_mempool_tx_source (embedded_tx_select.cpp) -- Mempool -> GBT txs[]
10+
// * make_reconstruct_closure_from_template (#82) -- [gentx] ++ other_txs
11+
// proving the SAME txs the template hands the miner are the txs that land in the
12+
// reconstructed broadcast block, byte-faithfully and in template order.
13+
//
14+
// Links the full dgb_coin codec (it compiles the tx serialization) and the
15+
// reconstruct closure. MUST be in BOTH build.yml --target allowlists (#143
16+
// NOT_BUILT trap). Per-coin isolation: src/impl/dgb/ only.
17+
// ---------------------------------------------------------------------------
18+
#include <cstdint>
19+
#include <functional>
20+
#include <stdexcept>
21+
#include <string>
22+
#include <vector>
23+
24+
#include <gtest/gtest.h>
25+
26+
#include <impl/dgb/coin/template_other_txs.hpp>
27+
#include <impl/dgb/coin/embedded_tx_select.hpp>
28+
#include <impl/dgb/coin/mempool.hpp>
29+
#include <impl/dgb/coin/transaction.hpp>
30+
#include <impl/dgb/coin/reconstruct_closure.hpp>
31+
32+
#include <core/pack.hpp>
33+
#include <core/uint256.hpp>
34+
#include <btclibs/util/strencodings.h>
35+
36+
using dgb::coin::Mempool;
37+
using dgb::coin::MutableTransaction;
38+
using dgb::coin::TxIn;
39+
using dgb::coin::TxOut;
40+
using dgb::coin::TX_WITH_WITNESS;
41+
using dgb::coin::TX_NO_WITNESS;
42+
using dgb::coin::compute_txid;
43+
using dgb::coin::make_mempool_tx_source;
44+
using dgb::coin::deserialize_template_tx;
45+
using dgb::coin::deserialize_template_other_txs;
46+
using dgb::coin::make_template_other_txs_fn;
47+
using dgb::coin::make_reconstruct_closure_from_template;
48+
using dgb::coin::reconstruct_won_block_from_template;
49+
using dgb::coin::unpack_gentx_coinbase;
50+
using dgb::coin::SmallBlockHeaderType;
51+
using dgb::coin::WonShareInputs;
52+
53+
namespace {
54+
55+
MutableTransaction tagged_tx(int64_t value, uint32_t index)
56+
{
57+
MutableTransaction tx;
58+
tx.version = 1;
59+
tx.locktime = 0;
60+
TxIn in;
61+
in.prevout.hash.SetNull();
62+
in.prevout.index = index;
63+
in.sequence = 0xffffffff;
64+
tx.vin.push_back(in);
65+
TxOut out;
66+
out.value = value;
67+
tx.vout.push_back(out);
68+
return tx;
69+
}
70+
71+
MutableTransaction make_gentx()
72+
{
73+
MutableTransaction tx;
74+
tx.version = 1;
75+
tx.locktime = 0;
76+
TxIn in;
77+
in.prevout.hash.SetNull();
78+
in.prevout.index = 0xffffffff; // coinbase
79+
in.sequence = 0xffffffff;
80+
tx.vin.push_back(in);
81+
TxOut out;
82+
out.value = 5000000000LL;
83+
tx.vout.push_back(out);
84+
return tx;
85+
}
86+
87+
std::vector<unsigned char> noseg_bytes(const MutableTransaction& tx)
88+
{
89+
auto packed = pack(TX_NO_WITNESS(tx));
90+
auto sp = packed.get_span();
91+
return std::vector<unsigned char>(
92+
reinterpret_cast<const unsigned char*>(sp.data()),
93+
reinterpret_cast<const unsigned char*>(sp.data()) + sp.size());
94+
}
95+
96+
std::string withwit_hex(const MutableTransaction& tx)
97+
{
98+
return HexStr(pack(TX_WITH_WITNESS(tx)).get_span());
99+
}
100+
101+
SmallBlockHeaderType make_small_header()
102+
{
103+
SmallBlockHeaderType h;
104+
h.m_version = 0x20000000;
105+
h.m_previous_block.SetHex("00000000000000000000000000000000000000000000000000000000deadbeef");
106+
h.m_timestamp = 1718700000;
107+
h.m_bits = 0x1a0fffff;
108+
h.m_nonce = 0x12345678;
109+
return h;
110+
}
111+
112+
uint256 won_hash()
113+
{
114+
uint256 h; h.SetHex("00000000000000000000000000000000000000000000000000000000000000a0");
115+
return h;
116+
}
117+
118+
} // namespace
119+
120+
// --- Test 1: round-trip -- the production template txs decode byte-faithfully -
121+
// Populate a Mempool, shape it through the PRODUCTION make_mempool_tx_source,
122+
// then decode the resulting transactions[] back through the bridge. Each decoded
123+
// tx must re-serialize (with-witness) to the exact `data` the template carried
124+
// and carry the exact `txid`, in template order.
125+
TEST(DgbTemplateOtherTxs, RoundTripsProductionTemplateTxs)
126+
{
127+
Mempool pool;
128+
MutableTransaction a = tagged_tx(10, 0);
129+
MutableTransaction b = tagged_tx(20, 1);
130+
ASSERT_TRUE(pool.add_tx(a));
131+
ASSERT_TRUE(pool.add_tx(b));
132+
pool.set_tx_fee(compute_txid(a), 700);
133+
pool.set_tx_fee(compute_txid(b), 300);
134+
135+
auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000);
136+
const auto sel = source();
137+
ASSERT_EQ(sel.transactions.size(), 2u);
138+
139+
const auto txs = deserialize_template_other_txs(sel.transactions);
140+
ASSERT_EQ(txs.size(), sel.transactions.size()); // template order preserved
141+
142+
for (size_t i = 0; i < txs.size(); ++i)
143+
{
144+
// Decoded tx re-serializes to the exact template `data` and `txid`.
145+
EXPECT_EQ(withwit_hex(txs[i]), sel.transactions[i]["data"].get<std::string>());
146+
EXPECT_EQ(compute_txid(txs[i]).GetHex(), sel.transactions[i]["txid"].get<std::string>());
147+
}
148+
}
149+
150+
// --- Test 2: empty / absent transactions[] -> empty vector (coinbase-only) ----
151+
TEST(DgbTemplateOtherTxs, EmptyTransactionsIsEmptyVector)
152+
{
153+
EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json::array()).empty());
154+
EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json(nullptr)).empty());
155+
}
156+
157+
// --- Test 3: malformed `data` (trailing byte) -> throws (fail-closed) ---------
158+
TEST(DgbTemplateOtherTxs, TrailingBytesThrow)
159+
{
160+
std::string good = withwit_hex(tagged_tx(10, 0));
161+
EXPECT_NO_THROW(deserialize_template_tx(good));
162+
EXPECT_THROW(deserialize_template_tx(good + "ff"), std::out_of_range); // junk byte
163+
}
164+
165+
// --- Test 4: end-to-end -- funded template txs land in the reconstructed block -
166+
// The bridge, wired as the reconstructor's template_other_txs_fn via a per-share
167+
// captured-transactions[] provider, reconstructs the SAME block that feeding the
168+
// deserialized txs straight into reconstruct_won_block_from_template produces:
169+
// [gentx] ++ the mempool-funded template txs, byte-identical.
170+
TEST(DgbTemplateOtherTxs, ReconstructsBlockWithFundedTemplateTxs)
171+
{
172+
Mempool pool;
173+
MutableTransaction a = tagged_tx(10, 0);
174+
MutableTransaction b = tagged_tx(20, 1);
175+
ASSERT_TRUE(pool.add_tx(a));
176+
ASSERT_TRUE(pool.add_tx(b));
177+
pool.set_tx_fee(compute_txid(a), 700);
178+
pool.set_tx_fee(compute_txid(b), 300);
179+
const auto sel = make_mempool_tx_source(pool, /*max_weight=*/4'000'000)();
180+
ASSERT_FALSE(sel.transactions.empty());
181+
182+
auto sh = make_small_header();
183+
::dgb::MerkleLink link;
184+
const uint256 won = won_hash();
185+
auto gentx_bytes = noseg_bytes(make_gentx());
186+
auto ug = unpack_gentx_coinbase(gentx_bytes);
187+
188+
// Expected: [gentx] ++ decoded-template-txs straight through the SSOT.
189+
const auto other = deserialize_template_other_txs(sel.transactions);
190+
const auto expected =
191+
reconstruct_won_block_from_template(sh, link, ug.tx, ug.txid, other);
192+
193+
// Actual: the run-loop shape -- closure pulls the per-share captured txs[]
194+
// through the bridge factory.
195+
auto txs_json = sel.transactions;
196+
auto closure = make_reconstruct_closure_from_template(
197+
[won, sh, link](const uint256& h) -> WonShareInputs {
198+
if (h != won) throw std::out_of_range("unknown share");
199+
return WonShareInputs{sh, link};
200+
},
201+
[gentx_bytes](const uint256&) { return gentx_bytes; },
202+
make_template_other_txs_fn(
203+
[txs_json](const uint256&) { return txs_json; }));
204+
205+
auto got = closure(won);
206+
ASSERT_TRUE(got.has_value());
207+
EXPECT_FALSE(got->first.empty());
208+
EXPECT_EQ(got->first, expected.bytes); // funded txs land in the block
209+
EXPECT_EQ(got->second, expected.hex);
210+
}
211+
212+
// --- Test 5: a bad captured provider fails the whole won block CLOSED ----------
213+
TEST(DgbTemplateOtherTxs, BadProviderFailsClosed)
214+
{
215+
auto sh = make_small_header();
216+
::dgb::MerkleLink link;
217+
const uint256 won = won_hash();
218+
auto gentx_bytes = noseg_bytes(make_gentx());
219+
220+
nlohmann::json bad = nlohmann::json::array();
221+
bad.push_back({{"data", withwit_hex(tagged_tx(10, 0)) + "ff"}}); // trailing junk
222+
223+
auto closure = make_reconstruct_closure_from_template(
224+
[won, sh, link](const uint256& h) -> WonShareInputs {
225+
if (h != won) throw std::out_of_range("unknown share");
226+
return WonShareInputs{sh, link};
227+
},
228+
[gentx_bytes](const uint256&) { return gentx_bytes; },
229+
make_template_other_txs_fn([bad](const uint256&) { return bad; }));
230+
231+
EXPECT_FALSE(closure(won).has_value()); // decode throw -> nullopt, no broadcast
232+
}

0 commit comments

Comments
 (0)