Skip to content

Commit 82eca8e

Browse files
committed
dgb(#82): wire CoinParams::subsidy_func into embedded coinbasevalue SSOT
Adds src/impl/dgb/coin/embedded_coinbase_value.hpp: the single place the DGB CoinParams::subsidy_func feeds a live coinbase-build path. Until now subsidy_func was populated in params.hpp (oracle-conformed) but had zero invocation sites; the live path only read m_coinbase_value off external-daemon GBT. embedded_coinbase_value(subsidy_func, height, fees) = subsidy_func(height)+fees, mirroring ltc/coin/template_builder.hpp coinbasevalue = subsidy + total_fees but sourcing the subsidy through the CoinParams SSOT (oracle decay schedule). resolve_coinbase_value() preserves the external-daemon fallback: a present GBT coinbasevalue is authoritative and returned verbatim; the embedded derivation is used only when no external daemon supplies one. Unset subsidy_func throws loudly. test/test_dgb_coinbase_value.cpp: 4-era boundary conformance through the subsidy_func indirection + fee composition + GBT-precedence + unset-throws (5/5). Registered in test/CMakeLists.txt and both build.yml --target allowlists (#143). Single-coin; no bitcoin_family/src/core/sibling-coin touch. External-daemon GBT path unchanged.
1 parent cb16987 commit 82eca8e

4 files changed

Lines changed: 182 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
test_phase4_embedded \
6868
test_mweb_builder \
6969
test_address_resolution test_compute_share_target \
70-
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \
70+
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test \
7171
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
7272
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \
7373
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
@@ -199,7 +199,7 @@ jobs:
199199
test_phase4_embedded \
200200
test_mweb_builder \
201201
test_address_resolution test_compute_share_target \
202-
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \
202+
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test \
203203
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
204204
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \
205205
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#pragma once
2+
// ============================================================================
3+
// embedded_coinbase_value.hpp — SSOT for the embedded-path coinbase value.
4+
//
5+
// p2pool's gentx pays out the block's coinbasevalue = block subsidy + the
6+
// fees of the transactions included in the template. On the external-daemon
7+
// path that figure arrives ready-made in the digibyted getblocktemplate
8+
// "coinbasevalue" field; on the embedded path (no external RPC) c2pool-dgb
9+
// must derive it locally — block_subsidy(height) + sum(tx fees).
10+
//
11+
// This is the SINGLE place the DGB CoinParams::subsidy_func feeds a live
12+
// coinbase-build path. Until this wiring, subsidy_func was populated in
13+
// params.hpp (-> CoinParams::subsidy, oracle-conformed, see
14+
// test/test_dgb_subsidy.cpp) but had ZERO invocation sites — the live path
15+
// only ever read m_coinbase_value off GBT. The embedded TemplateBuilder
16+
// (Stage 4c, src/impl/dgb/stratum/work_source.cpp) consumes this helper so
17+
// the embedded coinbasevalue and the external-daemon GBT coinbasevalue are
18+
// computed from one definition and can never silently diverge.
19+
//
20+
// HARD INVARIANT (project TODO, integrator 2026-06-18): the external-daemon
21+
// GBT fallback MUST PERSIST. resolve_coinbase_value() takes the GBT value
22+
// verbatim whenever it is present — digibyted already summed subsidy+fees
23+
// consensus-correctly, so the embedded derivation is a FALLBACK for the
24+
// no-external-daemon case, never an override of a live GBT figure.
25+
//
26+
// Pure + header-only: takes the subsidy_func callback (core::SubsidyFunc) and
27+
// integer fee totals, so it is directly unit-testable against the oracle
28+
// boundary vectors without standing up a node.
29+
// ============================================================================
30+
31+
#include <core/pow.hpp> // core::SubsidyFunc = std::function<uint64_t(uint32_t)>
32+
33+
#include <cstdint>
34+
#include <optional>
35+
#include <stdexcept>
36+
37+
namespace dgb::coin
38+
{
39+
40+
// Embedded-path coinbasevalue = subsidy_func(height) + total_fees.
41+
// Mirrors src/impl/ltc/coin/template_builder.hpp:
42+
// uint64_t coinbasevalue = subsidy + total_fees;
43+
// but sources the subsidy through the coin's CoinParams::subsidy_func SSOT
44+
// (the DGB oracle decay schedule) rather than a hardcoded halving formula.
45+
//
46+
// Throws std::logic_error if subsidy_func is unset — an empty std::function
47+
// here means the CoinParams factory was not wired, which must fail loudly at
48+
// the build site rather than silently pay a zero subsidy.
49+
inline uint64_t embedded_coinbase_value(const core::SubsidyFunc& subsidy_func,
50+
uint32_t height,
51+
uint64_t total_fees)
52+
{
53+
if (!subsidy_func)
54+
throw std::logic_error(
55+
"dgb::coin::embedded_coinbase_value: subsidy_func is unset "
56+
"(CoinParams factory not wired)");
57+
return subsidy_func(height) + total_fees;
58+
}
59+
60+
// Resolve the coinbasevalue for a template, preserving the external-daemon
61+
// path. When gbt_coinbasevalue is present (external digibyted GBT), it is
62+
// authoritative and returned verbatim — the embedded derivation is NOT used.
63+
// When absent (embedded TemplateBuilder, no external RPC), derive locally via
64+
// embedded_coinbase_value(subsidy_func, height, total_fees).
65+
inline uint64_t resolve_coinbase_value(const core::SubsidyFunc& subsidy_func,
66+
uint32_t height,
67+
uint64_t total_fees,
68+
std::optional<uint64_t> gbt_coinbasevalue)
69+
{
70+
if (gbt_coinbasevalue.has_value())
71+
return *gbt_coinbasevalue; // external-daemon fallback PERSISTS
72+
return embedded_coinbase_value(subsidy_func, height, total_fees);
73+
}
74+
75+
} // namespace dgb::coin

test/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ if (BUILD_TESTING AND GTest_FOUND)
1111
)
1212
gtest_discover_tests(test_dgb_subsidy DISCOVERY_MODE PRE_TEST)
1313

14+
# DGB embedded-path coinbasevalue SSOT (forward-only #82 wiring TODO):
15+
# embedded_coinbase_value.hpp routes CoinParams::subsidy_func(height)+fees
16+
# into the coinbasevalue and preserves the external-daemon GBT fallback.
17+
# Header-only like test_dgb_subsidy (config_coin.hpp subsidy is static); no
18+
# dgb runtime link. MUST also be in the build.yml --target allowlist (#143).
19+
add_executable(test_dgb_coinbase_value test_dgb_coinbase_value.cpp)
20+
target_link_libraries(test_dgb_coinbase_value PRIVATE
21+
GTest::gtest_main GTest::gtest
22+
btclibs
23+
yaml-cpp::yaml-cpp
24+
nlohmann_json::nlohmann_json
25+
${Boost_LIBRARIES}
26+
)
27+
gtest_discover_tests(test_dgb_coinbase_value DISCOVERY_MODE PRE_TEST)
28+
1429
add_executable(test_coin_broadcaster test_coin_broadcaster.cpp)
1530
target_link_libraries(test_coin_broadcaster PRIVATE
1631
GTest::gtest_main GTest::gtest

test/test_dgb_coinbase_value.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Forward-only TODO (integrator 2026-06-18): wire CoinParams::subsidy_func into
2+
// the embedded coinbase builder + a 4-era boundary conformance test, with the
3+
// external-daemon GBT fallback preserved.
4+
//
5+
// This guards src/impl/dgb/coin/embedded_coinbase_value.hpp — the SSOT that
6+
// feeds subsidy_func(height)+fees to the embedded TemplateBuilder coinbasevalue
7+
// (Stage 4c, work_source.cpp) and that takes a live digibyted GBT coinbasevalue
8+
// verbatim whenever present (the external-daemon fallback that MUST PERSIST).
9+
//
10+
// The subsidy values themselves are byte-exact vs the p2pool-dgb-scrypt oracle
11+
// (see test_dgb_subsidy.cpp). This test proves the *wiring*: that the embedded
12+
// coinbasevalue is derived THROUGH the CoinParams::subsidy_func indirection
13+
// (which params.hpp populates and which previously had zero invocation sites)
14+
// and that subsidy + fees compose correctly at all four reward-era boundaries.
15+
16+
#include <gtest/gtest.h>
17+
#include <cstdint>
18+
#include <optional>
19+
#include <stdexcept>
20+
21+
#include <core/pow.hpp> // core::SubsidyFunc
22+
#include <impl/dgb/config_coin.hpp> // dgb::CoinParams::subsidy (oracle SSOT)
23+
#include <impl/dgb/coin/embedded_coinbase_value.hpp>
24+
25+
namespace {
26+
27+
// IDENTICAL lambda to params.hpp `p.subsidy_func` — exercises the same
28+
// std::function indirection the live CoinParams carries.
29+
const core::SubsidyFunc kSubsidyFunc =
30+
[](uint32_t height) -> uint64_t { return dgb::CoinParams::subsidy(height); };
31+
32+
struct EraVec { uint32_t height; uint64_t subsidy; const char* era; };
33+
34+
// One pin on each side of every reward-era boundary. Expected subsidy values
35+
// are the p2pool-dgb-scrypt oracle vectors (test_dgb_subsidy.cpp).
36+
constexpr EraVec kEraBoundaries[] = {
37+
{67199, 8000000000ULL, "phase1-fixed last"},
38+
{67200, 7960000000ULL, "phase2 -0.5%/wk first"},
39+
{399999, 6746441103ULL, "phase2 last"},
40+
{400000, 2434410000ULL, "phase3 -1%/wk first"},
41+
{1429999, 2157824200ULL, "phase3 last"},
42+
{1430000, 1078500000ULL, "phase4 monthly-decay first"},
43+
};
44+
45+
} // namespace
46+
47+
// Zero fees: embedded coinbasevalue == the oracle subsidy, derived through the
48+
// subsidy_func callback (not the static call) at every era boundary.
49+
TEST(DgbCoinbaseValue, EmbeddedEqualsSubsidyAtEveryEraBoundary) {
50+
for (const auto& v : kEraBoundaries) {
51+
EXPECT_EQ(dgb::coin::embedded_coinbase_value(kSubsidyFunc, v.height, /*fees=*/0),
52+
v.subsidy)
53+
<< "embedded coinbasevalue diverged from oracle subsidy at " << v.era;
54+
}
55+
}
56+
57+
// Non-zero fees compose additively: coinbasevalue = subsidy + total_fees.
58+
TEST(DgbCoinbaseValue, AddsTransactionFees) {
59+
constexpr uint64_t kFees = 1234567ULL;
60+
for (const auto& v : kEraBoundaries) {
61+
EXPECT_EQ(dgb::coin::embedded_coinbase_value(kSubsidyFunc, v.height, kFees),
62+
v.subsidy + kFees)
63+
<< "fee addition wrong at " << v.era;
64+
}
65+
}
66+
67+
// External-daemon fallback PERSISTS: when a GBT coinbasevalue is present it is
68+
// authoritative and returned verbatim — the embedded derivation is bypassed,
69+
// even when it would differ.
70+
TEST(DgbCoinbaseValue, GbtValueTakesPrecedence) {
71+
constexpr uint32_t kHeight = 400000; // phase3 boundary
72+
constexpr uint64_t kGbt = 99999999999ULL; // deliberately != subsidy+fees
73+
EXPECT_EQ(dgb::coin::resolve_coinbase_value(kSubsidyFunc, kHeight, /*fees=*/500,
74+
std::optional<uint64_t>{kGbt}),
75+
kGbt);
76+
}
77+
78+
// No external daemon (nullopt): resolve derives locally via subsidy_func+fees.
79+
TEST(DgbCoinbaseValue, NoGbtDerivesEmbedded) {
80+
constexpr uint32_t kHeight = 400000;
81+
constexpr uint64_t kFees = 777ULL;
82+
EXPECT_EQ(dgb::coin::resolve_coinbase_value(kSubsidyFunc, kHeight, kFees, std::nullopt),
83+
dgb::CoinParams::subsidy(kHeight) + kFees);
84+
}
85+
86+
// An unset subsidy_func must fail LOUDLY (no silent zero-subsidy coinbase).
87+
TEST(DgbCoinbaseValue, UnsetSubsidyFuncThrows) {
88+
core::SubsidyFunc empty;
89+
EXPECT_THROW(dgb::coin::embedded_coinbase_value(empty, 400000, 0), std::logic_error);
90+
}

0 commit comments

Comments
 (0)