Skip to content

Commit fac36c3

Browse files
authored
dgb: end-to-end GBT-parity KAT for embedded coinbasevalue (#274)
Pin the assembled embedded coinbase-value path: a populated Mempool driven through the production make_mempool_tx_source -> EmbeddedCoinNode -> build_work_template wiring (main_dgb.cpp:233) must emit coinbasevalue == subsidy(next_height) + sum(included-tx fees) at every DGB reward era -- bit-for-bit the figure digibyted getblocktemplate reports. The prior tests pinned each link in isolation (tx-select shaping, stub-source fee fold, work-source era subsidy); none pinned the assembled chain equals GBT. Also pins behaviour-preserving displacement (embedded == GBT-present path when they agree) and the HARD INVARIANT that a present GBT coinbasevalue stays authoritative verbatim. Test-only, non-consensus: subsidy oracle is dgb::CoinParams::subsidy (p2pool-dgb-scrypt replica). Links the full dgb_coin codec like dgb_embedded_tx_select_test; added to CMake + both build.yml --target allowlists (#143 NOT_BUILT trap). 4/4 PASS. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent 009ac59 commit fac36c3

3 files changed

Lines changed: 242 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 \
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 \
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 \
8888
v37_test \
@@ -215,7 +215,7 @@ jobs:
215215
test_address_resolution test_compute_share_target \
216216
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 \
217217
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 \
218-
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 \
218+
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 \
219219
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
220220
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \
221221
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \

src/impl/dgb/test/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,22 @@ if (BUILD_TESTING AND GTest_FOUND)
244244
nlohmann_json::nlohmann_json)
245245
gtest_add_tests(dgb_embedded_tx_select_test "" AUTO)
246246

247+
# dgb_coinbase_value_parity_test: END-TO-END GBT-parity KAT. Drives a
248+
# populated Mempool through the production make_mempool_tx_source ->
249+
# EmbeddedCoinNode -> build_work_template path and asserts coinbasevalue ==
250+
# subsidy(next_height) + sum(mempool fees) (the digibyted GBT figure) at
251+
# every reward era. Compiles the tx serialization codec like
252+
# dgb_embedded_tx_select_test, so it links the same proven SCC set. MUST
253+
# also be in BOTH build.yml --target allowlists (#143 NOT_BUILT trap).
254+
add_executable(dgb_coinbase_value_parity_test coinbase_value_parity_test.cpp)
255+
target_link_libraries(dgb_coinbase_value_parity_test PRIVATE
256+
GTest::gtest_main GTest::gtest
257+
core dgb dgb_stratum
258+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
259+
dgb_coin pool sharechain
260+
nlohmann_json::nlohmann_json)
261+
gtest_add_tests(dgb_coinbase_value_parity_test "" AUTO)
262+
247263

248264
foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test)
249265
add_executable(${dgb_guard} ${dgb_guard}.cpp)
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// ---------------------------------------------------------------------------
2+
// dgb_coinbase_value_parity_test -- END-TO-END GBT-parity KAT for the embedded
3+
// coinbasevalue.
4+
//
5+
// The other dgb embedded tests pin each LINK of the chain in isolation:
6+
// * dgb_embedded_tx_select_test -- make_mempool_tx_source shapes a Mempool
7+
// selection and sums total_fees.
8+
// * dgb_embedded_coin_node_test -- a STUB tx source folds total_fees into
9+
// coinbasevalue via resolve_coinbase_value.
10+
// * dgb_work_source_test -- the stratum path derives subsidy at era
11+
// boundaries and honours a present GBT
12+
// coinbasevalue verbatim.
13+
//
14+
// What was NOT pinned anywhere: the ASSEMBLED chain. This test drives a
15+
// POPULATED embedded Mempool through the PRODUCTION make_mempool_tx_source ->
16+
// EmbeddedCoinNode -> build_work_template path (the exact wiring main_dgb.cpp:233
17+
// stands up) and asserts the emitted coinbasevalue is EXACTLY the figure
18+
// digibyted's getblocktemplate reports for the same template:
19+
//
20+
// coinbasevalue == subsidy(next_height) + sum(included-tx fees)
21+
//
22+
// This is the parity gate the integrator required (2026-06-20) before the
23+
// embedded subsidy+fees derivation may stand in for the external-daemon GBT
24+
// "coinbasevalue": the locally-built value must equal GBT bit-for-bit (so
25+
// displacing the fallback is behaviour-preserving), and the GBT figure must
26+
// stay authoritative when present (the HARD INVARIANT in embedded_coinbase_value.hpp).
27+
//
28+
// Oracle: dgb::CoinParams::subsidy (config_coin.hpp) is a bit-for-bit replica
29+
// of the p2pool-dgb-scrypt get_subsidy() oracle, so the subsidy half of the GBT
30+
// figure IS the spec. The era-boundary subsidy numbers are the captured oracle
31+
// vectors (shared with dgb_work_source_test / test_dgb_subsidy.cpp).
32+
//
33+
// Links the FULL dgb_coin codec (Mempool -> transaction.hpp) like
34+
// dgb_embedded_tx_select_test -- it exercises the real serialization shaper,
35+
// NOT the stub. MUST also be in BOTH build.yml --target allowlists (#143
36+
// NOT_BUILT trap).
37+
// ---------------------------------------------------------------------------
38+
39+
#include <cstdint>
40+
#include <optional>
41+
#include <vector>
42+
43+
#include <gtest/gtest.h>
44+
45+
#include <impl/dgb/coin/embedded_coin_node.hpp> // EmbeddedCoinNode, resolve_coinbase_value
46+
#include <impl/dgb/coin/embedded_tx_select.hpp> // make_mempool_tx_source (PRODUCTION source)
47+
#include <impl/dgb/coin/mempool.hpp> // Mempool, MutableTransaction
48+
#include <impl/dgb/coin/transaction.hpp> // compute_txid
49+
#include <impl/dgb/coin/header_chain.hpp> // HeaderChain, HeaderSample
50+
#include <impl/dgb/coin/dgb_block_algo.hpp> // DGB_BLOCK_VERSION_SCRYPT
51+
#include <impl/dgb/config_coin.hpp> // dgb::CoinParams::subsidy (oracle SSOT)
52+
#include <impl/dgb/config_pool.hpp> // dgb::PoolConfig::BLOCK_MAX_WEIGHT
53+
54+
#include <core/pow.hpp> // core::SubsidyFunc
55+
56+
using c2pool::dgb::HeaderChain;
57+
using c2pool::dgb::HeaderSample;
58+
using dgb::coin::EmbeddedCoinNode;
59+
using dgb::coin::Mempool;
60+
using dgb::coin::MutableTransaction;
61+
using dgb::coin::TxIn;
62+
using dgb::coin::TxOut;
63+
using dgb::coin::compute_txid;
64+
using dgb::coin::make_mempool_tx_source;
65+
using dgb::coin::build_work_template;
66+
using dgb::coin::resolve_coinbase_value;
67+
using dgb::coin::DGB_BLOCK_VERSION_SCRYPT;
68+
69+
namespace {
70+
71+
// Header nVersion with the Scrypt algo nibble (PRIMARY default | Scrypt==0) --
72+
// identical to dgb_embedded_coin_node_test.
73+
constexpr int32_t SCRYPT = 2 | DGB_BLOCK_VERSION_SCRYPT;
74+
75+
// The LIVE CoinParams indirection -- byte-identical to params.hpp p.subsidy_func
76+
// and dgb_work_source_test's fixture (dgb::CoinParams::subsidy = oracle SSOT).
77+
core::SubsidyFunc oracle_subsidy() {
78+
return [](uint32_t height) -> uint64_t { return dgb::CoinParams::subsidy(height); };
79+
}
80+
81+
// Captured p2pool-dgb-scrypt oracle subsidy on each side of every reward-era
82+
// boundary -- the SAME vectors dgb_work_source_test pins. Reusing the captured
83+
// numbers (not just the live function) double-locks the parity figure.
84+
struct EraVec { uint32_t height; uint64_t subsidy; const char* era; };
85+
constexpr EraVec kEraBoundaries[] = {
86+
{67199, 8000000000ULL, "phase1-fixed last"},
87+
{67200, 7960000000ULL, "phase2 -0.5%/wk first"},
88+
{399999, 6746441103ULL, "phase2 last"},
89+
{400000, 2434410000ULL, "phase3 -1%/wk first"},
90+
{1429999, 2157824200ULL, "phase3 last"},
91+
{1430000, 1078500000ULL, "phase4 monthly-decay first"},
92+
};
93+
94+
// Minimal, distinct fee-known tx tagged by its output value (mirrors the
95+
// builder in dgb_embedded_tx_select_test). `index` keeps the prevout (hence the
96+
// txid) distinct so set_tx_fee targets each tx unambiguously.
97+
MutableTransaction tagged_tx(int64_t value, uint32_t index)
98+
{
99+
MutableTransaction tx;
100+
tx.version = 1;
101+
tx.locktime = 0;
102+
TxIn in;
103+
in.prevout.hash.SetNull();
104+
in.prevout.index = index;
105+
in.sequence = 0xffffffff;
106+
tx.vin.push_back(in);
107+
TxOut out;
108+
out.value = value;
109+
tx.vout.push_back(out);
110+
return tx;
111+
}
112+
113+
// Seed a HeaderChain whose next_block_height() == `height`: set the base to
114+
// height-1, then append a single Scrypt continuity header so size()==1 and
115+
// next == (height-1)+1. (All era-boundary heights are > 0.) The appended
116+
// sample carries no block_hash, so tip_hash() stays nullopt -- previousblockhash
117+
// is held back, which is irrelevant to the coinbasevalue this KAT pins.
118+
void seed_chain_at(HeaderChain& hc, uint32_t height)
119+
{
120+
hc.set_base_height(height - 1);
121+
hc.validate_and_append(HeaderSample{SCRYPT, 1000, 100});
122+
}
123+
124+
} // namespace
125+
126+
// 1. THE PARITY GATE. A populated embedded Mempool, driven through the real
127+
// make_mempool_tx_source -> EmbeddedCoinNode -> build_work_template path,
128+
// must emit coinbasevalue == subsidy(next_height) + sum(mempool fees) at
129+
// every reward era -- bit-for-bit the figure digibyted's GBT reports.
130+
TEST(DgbCoinbaseValueParity, EmbeddedEqualsGbtFormulaWithMempoolFees)
131+
{
132+
// Three fee-known txs; their fees sum to the value digibyted folds into the
133+
// GBT coinbasevalue for an identical tx set.
134+
const uint64_t f1 = 11000, f2 = 23000, f3 = 4000;
135+
const uint64_t total_fees = f1 + f2 + f3;
136+
137+
for (const auto& v : kEraBoundaries) {
138+
HeaderChain hc;
139+
seed_chain_at(hc, v.height);
140+
ASSERT_EQ(hc.next_block_height(), v.height) << "seed height mismatch at " << v.era;
141+
142+
Mempool pool;
143+
MutableTransaction a = tagged_tx(10, 0);
144+
MutableTransaction b = tagged_tx(20, 1);
145+
MutableTransaction c = tagged_tx(30, 2);
146+
ASSERT_TRUE(pool.add_tx(a));
147+
ASSERT_TRUE(pool.add_tx(b));
148+
ASSERT_TRUE(pool.add_tx(c));
149+
pool.set_tx_fee(compute_txid(a), f1);
150+
pool.set_tx_fee(compute_txid(b), f2);
151+
pool.set_tx_fee(compute_txid(c), f3);
152+
153+
// PRODUCTION wiring: exactly main_dgb.cpp:233's construction.
154+
EmbeddedCoinNode node(
155+
hc, oracle_subsidy(),
156+
make_mempool_tx_source(pool, dgb::PoolConfig::BLOCK_MAX_WEIGHT));
157+
158+
const nlohmann::json t = build_work_template(node.make_inputs(/*curtime=*/1718800000));
159+
160+
// The GBT figure digibyted would report for this template.
161+
const uint64_t gbt_coinbasevalue = v.subsidy + total_fees;
162+
163+
EXPECT_EQ(t["height"].get<uint32_t>(), v.height);
164+
EXPECT_EQ(t["coinbasevalue"].get<uint64_t>(), gbt_coinbasevalue)
165+
<< "embedded coinbasevalue diverged from GBT subsidy+fees at " << v.era;
166+
// All three fee-known txs must be present in transactions[] (no silent drop).
167+
EXPECT_EQ(t["transactions"].size(), 3u) << "tx count wrong at " << v.era;
168+
// And the live oracle function agrees with the captured vector.
169+
EXPECT_EQ(dgb::CoinParams::subsidy(v.height), v.subsidy) << "oracle drift at " << v.era;
170+
}
171+
}
172+
173+
// 2. BEHAVIOUR-PRESERVING DISPLACEMENT. The embedded derivation (no GBT) must
174+
// reproduce EXACTLY what resolve_coinbase_value returns when handed the GBT
175+
// figure for the same inputs -- i.e. swapping the external-daemon
176+
// coinbasevalue for the local one changes nothing when they agree.
177+
TEST(DgbCoinbaseValueParity, EmbeddedIdenticalToGbtPresentPath)
178+
{
179+
const auto subsidy = oracle_subsidy();
180+
for (const auto& v : kEraBoundaries) {
181+
for (uint64_t fees : {uint64_t{0}, uint64_t{1}, uint64_t{987654321}}) {
182+
const uint64_t embedded =
183+
resolve_coinbase_value(subsidy, v.height, fees, /*gbt=*/std::nullopt);
184+
const uint64_t gbt_present =
185+
resolve_coinbase_value(subsidy, v.height, fees,
186+
/*gbt=*/std::optional<uint64_t>(v.subsidy + fees));
187+
EXPECT_EQ(embedded, gbt_present)
188+
<< "embedded != GBT-present at " << v.era << " fees=" << fees;
189+
EXPECT_EQ(embedded, v.subsidy + fees);
190+
}
191+
}
192+
}
193+
194+
// 3. GBT FALLBACK STAYS AUTHORITATIVE. When a GBT coinbasevalue is present it is
195+
// returned VERBATIM even if it diverges from the local derivation -- the
196+
// embedded path NEVER overrides a live digibyted figure (HARD INVARIANT).
197+
TEST(DgbCoinbaseValueParity, GbtPresentIsHonouredVerbatimEvenOnDivergence)
198+
{
199+
const auto subsidy = oracle_subsidy();
200+
const uint32_t h = 400000;
201+
// A deliberately "wrong" GBT figure (digibyted is authoritative regardless).
202+
const uint64_t bogus_gbt = 424242ULL;
203+
EXPECT_EQ(resolve_coinbase_value(subsidy, h, /*fees=*/5000, std::optional<uint64_t>(bogus_gbt)),
204+
bogus_gbt);
205+
}
206+
207+
// 4. EMPTY MEMPOOL -> PURE SUBSIDY (no fabricated fees). End-to-end through the
208+
// real source: an empty pool emits coinbasevalue == subsidy(next_height)
209+
// exactly and an empty transactions[] (truthful absence).
210+
TEST(DgbCoinbaseValueParity, EmptyMempoolEmitsPureSubsidy)
211+
{
212+
HeaderChain hc;
213+
seed_chain_at(hc, /*height=*/67200);
214+
215+
Mempool empty_pool;
216+
EmbeddedCoinNode node(
217+
hc, oracle_subsidy(),
218+
make_mempool_tx_source(empty_pool, dgb::PoolConfig::BLOCK_MAX_WEIGHT));
219+
220+
const nlohmann::json t = build_work_template(node.make_inputs(/*curtime=*/42));
221+
EXPECT_EQ(t["coinbasevalue"].get<uint64_t>(), dgb::CoinParams::subsidy(67200));
222+
EXPECT_TRUE(t["transactions"].is_array());
223+
EXPECT_TRUE(t["transactions"].empty());
224+
}

0 commit comments

Comments
 (0)