Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
75 changes: 75 additions & 0 deletions src/impl/dgb/coin/embedded_coinbase_value.hpp
Original file line number Diff line number Diff line change
@@ -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/pow.hpp> // core::SubsidyFunc = std::function<uint64_t(uint32_t)>

#include <cstdint>
#include <optional>
#include <stdexcept>

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<uint64_t> 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
15 changes: 15 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions test/test_dgb_coinbase_value.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>
#include <cstdint>
#include <optional>
#include <stdexcept>

#include <core/pow.hpp> // core::SubsidyFunc
#include <impl/dgb/config_coin.hpp> // dgb::CoinParams::subsidy (oracle SSOT)
#include <impl/dgb/coin/embedded_coinbase_value.hpp>

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<uint64_t>{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);
}
Loading