|
| 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