From 0754cca11c84ce39955e4dc9e73a6a74bd589ac9 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 19 Jun 2026 04:31:46 +0000 Subject: [PATCH] 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. --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/embedded_coinbase_value.hpp | 75 ++++++++++++++++ test/CMakeLists.txt | 15 ++++ test/test_dgb_coinbase_value.cpp | 90 +++++++++++++++++++ 4 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/coin/embedded_coinbase_value.hpp create mode 100644 test/test_dgb_coinbase_value.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9993d14e..cf5764542 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ + test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ @@ -199,7 +199,7 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ + test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ diff --git a/src/impl/dgb/coin/embedded_coinbase_value.hpp b/src/impl/dgb/coin/embedded_coinbase_value.hpp new file mode 100644 index 000000000..48d0457bd --- /dev/null +++ b/src/impl/dgb/coin/embedded_coinbase_value.hpp @@ -0,0 +1,75 @@ +#pragma once +// ============================================================================ +// embedded_coinbase_value.hpp — SSOT for the embedded-path coinbase value. +// +// p2pool's gentx pays out the block's coinbasevalue = block subsidy + the +// fees of the transactions included in the template. On the external-daemon +// path that figure arrives ready-made in the digibyted getblocktemplate +// "coinbasevalue" field; on the embedded path (no external RPC) c2pool-dgb +// must derive it locally — block_subsidy(height) + sum(tx fees). +// +// This is the SINGLE place the DGB CoinParams::subsidy_func feeds a live +// coinbase-build path. Until this wiring, subsidy_func was populated in +// params.hpp (-> CoinParams::subsidy, oracle-conformed, see +// test/test_dgb_subsidy.cpp) but had ZERO invocation sites — the live path +// only ever read m_coinbase_value off GBT. The embedded TemplateBuilder +// (Stage 4c, src/impl/dgb/stratum/work_source.cpp) consumes this helper so +// the embedded coinbasevalue and the external-daemon GBT coinbasevalue are +// computed from one definition and can never silently diverge. +// +// HARD INVARIANT (project TODO, integrator 2026-06-18): the external-daemon +// GBT fallback MUST PERSIST. resolve_coinbase_value() takes the GBT value +// verbatim whenever it is present — digibyted already summed subsidy+fees +// consensus-correctly, so the embedded derivation is a FALLBACK for the +// no-external-daemon case, never an override of a live GBT figure. +// +// Pure + header-only: takes the subsidy_func callback (core::SubsidyFunc) and +// integer fee totals, so it is directly unit-testable against the oracle +// boundary vectors without standing up a node. +// ============================================================================ + +#include // core::SubsidyFunc = std::function + +#include +#include +#include + +namespace dgb::coin +{ + +// Embedded-path coinbasevalue = subsidy_func(height) + total_fees. +// Mirrors src/impl/ltc/coin/template_builder.hpp: +// uint64_t coinbasevalue = subsidy + total_fees; +// but sources the subsidy through the coin's CoinParams::subsidy_func SSOT +// (the DGB oracle decay schedule) rather than a hardcoded halving formula. +// +// Throws std::logic_error if subsidy_func is unset — an empty std::function +// here means the CoinParams factory was not wired, which must fail loudly at +// the build site rather than silently pay a zero subsidy. +inline uint64_t embedded_coinbase_value(const core::SubsidyFunc& subsidy_func, + uint32_t height, + uint64_t total_fees) +{ + if (!subsidy_func) + throw std::logic_error( + "dgb::coin::embedded_coinbase_value: subsidy_func is unset " + "(CoinParams factory not wired)"); + return subsidy_func(height) + total_fees; +} + +// Resolve the coinbasevalue for a template, preserving the external-daemon +// path. When gbt_coinbasevalue is present (external digibyted GBT), it is +// authoritative and returned verbatim — the embedded derivation is NOT used. +// When absent (embedded TemplateBuilder, no external RPC), derive locally via +// embedded_coinbase_value(subsidy_func, height, total_fees). +inline uint64_t resolve_coinbase_value(const core::SubsidyFunc& subsidy_func, + uint32_t height, + uint64_t total_fees, + std::optional gbt_coinbasevalue) +{ + if (gbt_coinbasevalue.has_value()) + return *gbt_coinbasevalue; // external-daemon fallback PERSISTS + return embedded_coinbase_value(subsidy_func, height, total_fees); +} + +} // namespace dgb::coin diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0c2dd218f..7008c6332 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -11,6 +11,21 @@ if (BUILD_TESTING AND GTest_FOUND) ) gtest_discover_tests(test_dgb_subsidy DISCOVERY_MODE PRE_TEST) + # DGB embedded-path coinbasevalue SSOT (forward-only #82 wiring TODO): + # embedded_coinbase_value.hpp routes CoinParams::subsidy_func(height)+fees + # into the coinbasevalue and preserves the external-daemon GBT fallback. + # Header-only like test_dgb_subsidy (config_coin.hpp subsidy is static); no + # dgb runtime link. MUST also be in the build.yml --target allowlist (#143). + add_executable(test_dgb_coinbase_value test_dgb_coinbase_value.cpp) + target_link_libraries(test_dgb_coinbase_value PRIVATE + GTest::gtest_main GTest::gtest + btclibs + yaml-cpp::yaml-cpp + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + gtest_discover_tests(test_dgb_coinbase_value DISCOVERY_MODE PRE_TEST) + add_executable(test_coin_broadcaster test_coin_broadcaster.cpp) target_link_libraries(test_coin_broadcaster PRIVATE GTest::gtest_main GTest::gtest diff --git a/test/test_dgb_coinbase_value.cpp b/test/test_dgb_coinbase_value.cpp new file mode 100644 index 000000000..ed26630ff --- /dev/null +++ b/test/test_dgb_coinbase_value.cpp @@ -0,0 +1,90 @@ +// Forward-only TODO (integrator 2026-06-18): wire CoinParams::subsidy_func into +// the embedded coinbase builder + a 4-era boundary conformance test, with the +// external-daemon GBT fallback preserved. +// +// This guards src/impl/dgb/coin/embedded_coinbase_value.hpp — the SSOT that +// feeds subsidy_func(height)+fees to the embedded TemplateBuilder coinbasevalue +// (Stage 4c, work_source.cpp) and that takes a live digibyted GBT coinbasevalue +// verbatim whenever present (the external-daemon fallback that MUST PERSIST). +// +// The subsidy values themselves are byte-exact vs the p2pool-dgb-scrypt oracle +// (see test_dgb_subsidy.cpp). This test proves the *wiring*: that the embedded +// coinbasevalue is derived THROUGH the CoinParams::subsidy_func indirection +// (which params.hpp populates and which previously had zero invocation sites) +// and that subsidy + fees compose correctly at all four reward-era boundaries. + +#include +#include +#include +#include + +#include // core::SubsidyFunc +#include // dgb::CoinParams::subsidy (oracle SSOT) +#include + +namespace { + +// IDENTICAL lambda to params.hpp `p.subsidy_func` — exercises the same +// std::function indirection the live CoinParams carries. +const core::SubsidyFunc kSubsidyFunc = + [](uint32_t height) -> uint64_t { return dgb::CoinParams::subsidy(height); }; + +struct EraVec { uint32_t height; uint64_t subsidy; const char* era; }; + +// One pin on each side of every reward-era boundary. Expected subsidy values +// are the p2pool-dgb-scrypt oracle vectors (test_dgb_subsidy.cpp). +constexpr EraVec kEraBoundaries[] = { + {67199, 8000000000ULL, "phase1-fixed last"}, + {67200, 7960000000ULL, "phase2 -0.5%/wk first"}, + {399999, 6746441103ULL, "phase2 last"}, + {400000, 2434410000ULL, "phase3 -1%/wk first"}, + {1429999, 2157824200ULL, "phase3 last"}, + {1430000, 1078500000ULL, "phase4 monthly-decay first"}, +}; + +} // namespace + +// Zero fees: embedded coinbasevalue == the oracle subsidy, derived through the +// subsidy_func callback (not the static call) at every era boundary. +TEST(DgbCoinbaseValue, EmbeddedEqualsSubsidyAtEveryEraBoundary) { + for (const auto& v : kEraBoundaries) { + EXPECT_EQ(dgb::coin::embedded_coinbase_value(kSubsidyFunc, v.height, /*fees=*/0), + v.subsidy) + << "embedded coinbasevalue diverged from oracle subsidy at " << v.era; + } +} + +// Non-zero fees compose additively: coinbasevalue = subsidy + total_fees. +TEST(DgbCoinbaseValue, AddsTransactionFees) { + constexpr uint64_t kFees = 1234567ULL; + for (const auto& v : kEraBoundaries) { + EXPECT_EQ(dgb::coin::embedded_coinbase_value(kSubsidyFunc, v.height, kFees), + v.subsidy + kFees) + << "fee addition wrong at " << v.era; + } +} + +// External-daemon fallback PERSISTS: when a GBT coinbasevalue is present it is +// authoritative and returned verbatim — the embedded derivation is bypassed, +// even when it would differ. +TEST(DgbCoinbaseValue, GbtValueTakesPrecedence) { + constexpr uint32_t kHeight = 400000; // phase3 boundary + constexpr uint64_t kGbt = 99999999999ULL; // deliberately != subsidy+fees + EXPECT_EQ(dgb::coin::resolve_coinbase_value(kSubsidyFunc, kHeight, /*fees=*/500, + std::optional{kGbt}), + kGbt); +} + +// No external daemon (nullopt): resolve derives locally via subsidy_func+fees. +TEST(DgbCoinbaseValue, NoGbtDerivesEmbedded) { + constexpr uint32_t kHeight = 400000; + constexpr uint64_t kFees = 777ULL; + EXPECT_EQ(dgb::coin::resolve_coinbase_value(kSubsidyFunc, kHeight, kFees, std::nullopt), + dgb::CoinParams::subsidy(kHeight) + kFees); +} + +// An unset subsidy_func must fail LOUDLY (no silent zero-subsidy coinbase). +TEST(DgbCoinbaseValue, UnsetSubsidyFuncThrows) { + core::SubsidyFunc empty; + EXPECT_THROW(dgb::coin::embedded_coinbase_value(empty, 400000, 0), std::logic_error); +}