Skip to content

Commit b0e1f3f

Browse files
committed
dgb: per-job template-retention seam — captured_template_txs_fn provider (#271)
The won-block reconstructor frames the broadcast block as [gentx] ++ other_txs, where other_txs is the captured-GBT template the won share was mined against. The into that seam but defers the provider itself to the run-loop. main_dgb installs an interim mempool RE-SELECTION lambda, merkle-consistent ONLY while the mempool is static across the won window (regtest); in prod any mempool mutation between job hand-out and won-block diverges the set -> wrong merkle root -> daemon-rejected block -> lost reward. coin/template_capture.hpp removes that race: TemplateCapture retains each handed-out template transactions[] keyed by the resulting share_hash and replays it verbatim at won-block. provider() yields the captured_template_txs_fn make_template_other_txs_fn consumes. Miss -> empty array -> a VALID coinbase-only block (the reconstructor empty contract), NOT fail-closed: a missing template must not forfeit the subsidy. Bounded FIFO so memory is O(capacity), not O(jobs). Thread-safe (capture on the job/mint path, provide on the COMPUTE thread inside the closure). dgb_template_capture_test (5/5): capture->provide json round-trip; miss replays empty; composed through the #299 bridge a HIT decodes byte-faithfully to the MutableTransaction vector deserialize_template_other_txs yields directly and a MISS to empty; overwrite- same-hash keeps one entry; FIFO eviction drops the oldest. Stacks on #299. Fenced to src/impl/dgb/; no p2pool-merged-v36 surface. Both build.yml allowlists updated (#143).
1 parent 18f0d48 commit b0e1f3f

4 files changed

Lines changed: 334 additions & 0 deletions

File tree

.github/workflows/build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ 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+
<<<<<<< HEAD
8586
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 dgb_aux_parent_coinbase_parity_test \
87+
=======
88+
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_template_capture_test dgb_coinbase_value_parity_test dgb_submit_classify_test \
89+
>>>>>>> 38094399e (dgb: per-job template-retention seam — captured_template_txs_fn provider (#271))
8690
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
8791
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 \
8892
-j$(nproc)
@@ -214,7 +218,11 @@ jobs:
214218
test_address_resolution test_compute_share_target \
215219
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 \
216220
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 \
221+
<<<<<<< HEAD
217222
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 dgb_aux_parent_coinbase_parity_test \
223+
=======
224+
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_template_capture_test dgb_coinbase_value_parity_test dgb_submit_classify_test \
225+
>>>>>>> 38094399e (dgb: per-job template-retention seam — captured_template_txs_fn provider (#271))
218226
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
219227
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 \
220228
v37_test \
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// ---------------------------------------------------------------------------
2+
// template_capture.hpp -- the per-job template-retention seam (#271): the
3+
// PRODUCTION captured_template_txs_fn provider that feeds the won-block
4+
// reconstructor's template_other_txs_fn through the #299 deserialize bridge
5+
// (template_other_txs.hpp, make_template_other_txs_fn).
6+
//
7+
// WHY a capture store, not mempool re-selection:
8+
// A won share commits to the GBT template it was MINED AGAINST at job hand-
9+
// out. The reconstructed broadcast block's non-coinbase set MUST be that
10+
// template's transactions[] in template order, because the share's merkle
11+
// root was computed over [gentx] ++ those txs. main_dgb's interim
12+
// template_other_txs_fn RE-SELECTS the live embedded mempool, which is
13+
// merkle-consistent ONLY while the mempool is static across the won window
14+
// (true under regtest, FALSE in prod: any mempool mutation between hand-out
15+
// and won-block diverges the set -> wrong merkle root -> daemon-rejected
16+
// block -> lost reward). This store removes that race: capture the template's
17+
// transactions[] at hand-out keyed by the resulting share_hash, replay it
18+
// verbatim at won-block.
19+
//
20+
// SSOT split: this header owns ONLY the keyed retain/replay + bounded eviction.
21+
// The transactions[] -> MutableTransaction decode is template_other_txs.hpp
22+
// (#299); compose make_template_other_txs_fn(capture.provider()) to get the
23+
// template_other_txs_fn make_reconstruct_closure_from_template installs.
24+
//
25+
// Miss policy: an absent share_hash yields an EMPTY transactions[] (json
26+
// ::array()) -> the bridge decodes to an empty other_txs -> a VALID coinbase-
27+
// only won block (the reconstructor's documented empty contract), NOT a fail-
28+
// closed nullopt. A won block that loses its fee txs is still a valid, network-
29+
// accepted block carrying the full subsidy; dropping it on a capture miss would
30+
// forfeit the subsidy too. Logged so a miss is never silent.
31+
//
32+
// Bounded: jobs are handed out continuously; the store keeps the most recent
33+
// `capacity` templates (FIFO eviction) so memory is O(capacity), not O(jobs).
34+
// A won share is reconstructed within a few jobs of hand-out, so a modest
35+
// capacity covers every realistic won-window.
36+
//
37+
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE.
38+
// Thread-safe: capture() runs on the job/mint path, provider() fires on the
39+
// COMPUTE thread inside the won-block closure -- guarded by one mutex.
40+
// ---------------------------------------------------------------------------
41+
#pragma once
42+
43+
#include <cstddef>
44+
#include <deque>
45+
#include <functional>
46+
#include <iostream>
47+
#include <mutex>
48+
#include <string>
49+
#include <unordered_map>
50+
#include <utility>
51+
52+
#include <nlohmann/json.hpp>
53+
54+
#include <core/uint256.hpp>
55+
56+
namespace dgb
57+
{
58+
namespace coin
59+
{
60+
61+
// Per-job captured-template store keyed by share_hash. Retains each handed-out
62+
// template's transactions[] (the conformant GBT shape: array of {data,txid,
63+
// hash,fee}) so the won-block reconstructor can replay the exact non-coinbase
64+
// set the share committed to.
65+
class TemplateCapture
66+
{
67+
public:
68+
static constexpr std::size_t DEFAULT_CAPACITY = 256;
69+
70+
explicit TemplateCapture(std::size_t capacity = DEFAULT_CAPACITY)
71+
: m_capacity(capacity == 0 ? 1 : capacity) {}
72+
73+
// Retain `transactions` (a GBT transactions[] array) for `share_hash`.
74+
// Overwrites an existing entry for the same hash WITHOUT enqueuing the key
75+
// twice (so eviction stays in true insertion order). FIFO-evicts the oldest
76+
// once a new key would exceed capacity.
77+
void capture(const uint256& share_hash, nlohmann::json transactions)
78+
{
79+
const std::string key = share_hash.GetHex();
80+
std::lock_guard<std::mutex> lk(m_mtx);
81+
auto it = m_store.find(key);
82+
if (it == m_store.end())
83+
{
84+
if (m_store.size() >= m_capacity && !m_order.empty())
85+
{
86+
m_store.erase(m_order.front());
87+
m_order.pop_front();
88+
}
89+
m_order.push_back(key);
90+
}
91+
m_store[key] = std::move(transactions);
92+
}
93+
94+
// Replay the captured transactions[] for `share_hash`. Returns an empty
95+
// json::array() on a miss (-> coinbase-only valid block), logged.
96+
nlohmann::json provide(const uint256& share_hash) const
97+
{
98+
const std::string key = share_hash.GetHex();
99+
std::lock_guard<std::mutex> lk(m_mtx);
100+
auto it = m_store.find(key);
101+
if (it == m_store.end())
102+
{
103+
std::cout << "[DGB-POOL-BLOCK] template-capture MISS for won share "
104+
<< key.substr(0, 16)
105+
<< " -- reconstructing coinbase-only (no retained template)"
106+
<< std::endl;
107+
return nlohmann::json::array();
108+
}
109+
return it->second;
110+
}
111+
112+
// The captured_template_txs_fn make_template_other_txs_fn (#299) consumes.
113+
// The returned closure captures `this`; the TemplateCapture MUST outlive the
114+
// reconstruct closure it is installed into (it does in main_dgb: a run-loop-
115+
// scoped member outliving the tracker callback).
116+
std::function<nlohmann::json(const uint256&)> provider() const
117+
{
118+
return [this](const uint256& h) { return provide(h); };
119+
}
120+
121+
std::size_t size() const
122+
{
123+
std::lock_guard<std::mutex> lk(m_mtx);
124+
return m_store.size();
125+
}
126+
127+
private:
128+
const std::size_t m_capacity;
129+
mutable std::mutex m_mtx;
130+
std::unordered_map<std::string, nlohmann::json> m_store;
131+
std::deque<std::string> m_order;
132+
};
133+
134+
} // namespace coin
135+
} // namespace dgb

src/impl/dgb/test/CMakeLists.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,5 +451,24 @@ if (BUILD_TESTING AND GTest_FOUND)
451451
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
452452
dgb_coin pool sharechain)
453453
gtest_add_tests(dgb_aux_parent_coinbase_parity_test "" AUTO)
454+
# --- #271 per-job template-retention seam (coin/template_capture.hpp) ------
455+
# dgb_template_capture_test: pins the PRODUCTION captured_template_txs_fn the
456+
# won-block reconstructor's template_other_txs_fn is built from (via the #299
457+
# make_template_other_txs_fn bridge) -- capture(share,txs[]) -> provide(share)
458+
# replays the exact template, a MISS replays empty (coinbase-only valid, NOT
459+
# fail-closed), and composed through the bridge a HIT decodes to the same
460+
# MutableTransaction vector deserialize_template_other_txs yields directly.
461+
# Replaces main_dgb's interim mempool re-selection lambda (static-mempool-
462+
# only). Compiles the tx codec + bridge, so it links the same embedded_tx_
463+
# select SCC set as dgb_template_other_txs_test. MUST also be in BOTH build.yml
464+
# --target allowlists (#143 NOT_BUILT trap).
465+
add_executable(dgb_template_capture_test template_capture_test.cpp)
466+
target_link_libraries(dgb_template_capture_test PRIVATE
467+
GTest::gtest_main GTest::gtest
468+
core dgb dgb_stratum
469+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
470+
dgb_coin pool sharechain
471+
nlohmann_json::nlohmann_json)
472+
gtest_add_tests(dgb_template_capture_test "" AUTO)
454473

455474
endif()
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// ---------------------------------------------------------------------------
2+
// dgb_template_capture_test -- pins coin/template_capture.hpp, the per-job
3+
// template-retention seam (#271): the PRODUCTION captured_template_txs_fn the
4+
// won-block reconstructor's template_other_txs_fn is built from (via the #299
5+
// make_template_other_txs_fn bridge), replacing main_dgb's interim mempool
6+
// RE-SELECTION lambda (merkle-consistent only on a static mempool).
7+
//
8+
// What it pins:
9+
// * capture(share_hash, transactions[]) -> provide(share_hash) replays the
10+
// SAME GBT transactions[] byte-faithfully (json-equal), keyed by share.
11+
// * a capture MISS replays an empty array -> coinbase-only valid block (NOT
12+
// fail-closed), so a missing template never forfeits the won subsidy.
13+
// * make_template_other_txs_fn(capture.provider()) decodes a HIT to the exact
14+
// MutableTransaction vector deserialize_template_other_txs yields directly
15+
// from the same template -- i.e. the capture path is transparent to the
16+
// #299 bridge -- and a MISS to an empty other_txs vector.
17+
// * overwrite-same-hash keeps one entry (latest wins); FIFO eviction bounds
18+
// the store and drops the OLDEST template once over capacity.
19+
//
20+
// Links the full dgb_coin codec + reconstruct closure (it composes the #299
21+
// bridge over the production make_mempool_tx_source template). MUST be in BOTH
22+
// build.yml --target allowlists (#143 NOT_BUILT trap). Per-coin isolation:
23+
// src/impl/dgb/ only.
24+
// ---------------------------------------------------------------------------
25+
#include <cstdint>
26+
#include <functional>
27+
#include <string>
28+
#include <vector>
29+
30+
#include <gtest/gtest.h>
31+
32+
#include <impl/dgb/coin/template_capture.hpp>
33+
#include <impl/dgb/coin/template_other_txs.hpp>
34+
#include <impl/dgb/coin/embedded_tx_select.hpp>
35+
#include <impl/dgb/coin/mempool.hpp>
36+
#include <impl/dgb/coin/transaction.hpp>
37+
38+
#include <core/pack.hpp>
39+
#include <core/uint256.hpp>
40+
#include <btclibs/util/strencodings.h>
41+
42+
using dgb::coin::Mempool;
43+
using dgb::coin::MutableTransaction;
44+
using dgb::coin::TxIn;
45+
using dgb::coin::TxOut;
46+
using dgb::coin::TX_WITH_WITNESS;
47+
using dgb::coin::compute_txid;
48+
using dgb::coin::make_mempool_tx_source;
49+
using dgb::coin::deserialize_template_other_txs;
50+
using dgb::coin::make_template_other_txs_fn;
51+
using dgb::coin::TemplateCapture;
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+
std::string withwit_hex(const MutableTransaction& tx)
72+
{
73+
return HexStr(pack(TX_WITH_WITNESS(tx)).get_span());
74+
}
75+
76+
uint256 hash_for(const char* hex)
77+
{
78+
uint256 h; h.SetHex(hex);
79+
return h;
80+
}
81+
82+
// A production-shaped GBT transactions[] from a populated mempool.
83+
nlohmann::json sample_template_txs()
84+
{
85+
Mempool pool;
86+
MutableTransaction a = tagged_tx(11, 0);
87+
MutableTransaction b = tagged_tx(22, 1);
88+
pool.add_tx(a);
89+
pool.add_tx(b);
90+
pool.set_tx_fee(compute_txid(a), 700);
91+
pool.set_tx_fee(compute_txid(b), 300);
92+
return make_mempool_tx_source(pool, /*max_weight=*/4'000'000)().transactions;
93+
}
94+
95+
const char* H_A = "00000000000000000000000000000000000000000000000000000000000000a1";
96+
const char* H_B = "00000000000000000000000000000000000000000000000000000000000000b2";
97+
const char* H_C = "00000000000000000000000000000000000000000000000000000000000000c3";
98+
const char* H_MISS = "00000000000000000000000000000000000000000000000000000000deadbeef";
99+
100+
} // namespace
101+
102+
// --- Test 1: capture -> provide round-trips the template byte-faithfully ------
103+
TEST(DgbTemplateCapture, CaptureProvideRoundTrip)
104+
{
105+
TemplateCapture cap;
106+
const auto txs = sample_template_txs();
107+
ASSERT_EQ(txs.size(), 2u);
108+
109+
cap.capture(hash_for(H_A), txs);
110+
EXPECT_EQ(cap.size(), 1u);
111+
// json-equal: the exact transactions[] handed in comes back out.
112+
EXPECT_EQ(cap.provide(hash_for(H_A)), txs);
113+
}
114+
115+
// --- Test 2: a MISS replays an empty array (coinbase-only, not fail-closed) ---
116+
TEST(DgbTemplateCapture, MissReplaysEmptyArray)
117+
{
118+
TemplateCapture cap;
119+
cap.capture(hash_for(H_A), sample_template_txs());
120+
121+
const auto miss = cap.provide(hash_for(H_MISS));
122+
ASSERT_TRUE(miss.is_array());
123+
EXPECT_TRUE(miss.empty());
124+
}
125+
126+
// --- Test 3: composed through the #299 bridge, a HIT decodes to the exact tx --
127+
// vector deserialize_template_other_txs yields directly from the same template;
128+
// a MISS decodes to an empty other_txs vector. Proves the capture path is
129+
// transparent to the reconstructor's template_other_txs_fn.
130+
TEST(DgbTemplateCapture, ComposedWithBridgeHitAndMiss)
131+
{
132+
TemplateCapture cap;
133+
const auto txs = sample_template_txs();
134+
cap.capture(hash_for(H_A), txs);
135+
136+
auto other_txs_fn = make_template_other_txs_fn(cap.provider());
137+
138+
// HIT: identical to decoding the template directly, in template order.
139+
const auto via_capture = other_txs_fn(hash_for(H_A));
140+
const auto direct = deserialize_template_other_txs(txs);
141+
ASSERT_EQ(via_capture.size(), direct.size());
142+
ASSERT_EQ(via_capture.size(), 2u);
143+
for (size_t i = 0; i < via_capture.size(); ++i)
144+
EXPECT_EQ(withwit_hex(via_capture[i]), withwit_hex(direct[i]));
145+
146+
// MISS: empty other_txs -> the reconstructor frames a coinbase-only block.
147+
EXPECT_TRUE(other_txs_fn(hash_for(H_MISS)).empty());
148+
}
149+
150+
// --- Test 4: overwrite the same share_hash keeps one entry (latest wins) ------
151+
TEST(DgbTemplateCapture, OverwriteSameHashLatestWins)
152+
{
153+
TemplateCapture cap;
154+
cap.capture(hash_for(H_A), nlohmann::json::array()); // empty first
155+
cap.capture(hash_for(H_A), sample_template_txs()); // then 2-tx template
156+
EXPECT_EQ(cap.size(), 1u); // not duplicated
157+
EXPECT_EQ(cap.provide(hash_for(H_A)).size(), 2u); // latest content
158+
}
159+
160+
// --- Test 5: FIFO eviction bounds the store, drops the OLDEST template --------
161+
TEST(DgbTemplateCapture, BoundedFifoEvictsOldest)
162+
{
163+
TemplateCapture cap(/*capacity=*/2);
164+
cap.capture(hash_for(H_A), sample_template_txs());
165+
cap.capture(hash_for(H_B), sample_template_txs());
166+
cap.capture(hash_for(H_C), sample_template_txs()); // evicts H_A
167+
168+
EXPECT_EQ(cap.size(), 2u);
169+
EXPECT_TRUE(cap.provide(hash_for(H_A)).empty()); // oldest evicted -> miss
170+
EXPECT_EQ(cap.provide(hash_for(H_B)).size(), 2u); // newest retained
171+
EXPECT_EQ(cap.provide(hash_for(H_C)).size(), 2u);
172+
}

0 commit comments

Comments
 (0)