diff --git a/src/impl/dgb/config_coin.hpp b/src/impl/dgb/config_coin.hpp index f4340b74f..12fb02a39 100644 --- a/src/impl/dgb/config_coin.hpp +++ b/src/impl/dgb/config_coin.hpp @@ -72,34 +72,66 @@ class CoinParams static constexpr uint32_t DUMB_SCRYPT_DIFF = 65536; // 2^16 // ----------------------------------------------------------------------- - // DGB subsidy schedule (3-phase decay) - // Source: p2pool-dgb-scrypt oracle bitcoin/networks/digibyte.py + // DGB subsidy schedule -- EXACT replica of the p2pool-dgb-scrypt oracle + // get_subsidy() (bitcoin/networks/digibyte.py, IDENTIFIER 4B62545B1A631AFE). + // V36 conformance = parity with DGB's OWN canonical reference, so the + // oracle behaviour IS the spec -- including COIN=1e6 (NOT 1e8) and the + // weeks/months + 1 decay count. These are not quirks to "fix" (card #156). + // 3-bucket: COMPAT (pre-v36 per-coin baseline, temporary for the crossing; + // NOT a v36-native cross-coin standardization). + // + // Integer math mirrors the oracle bit-for-bit: each decay step is + // q -= q / N (truncating division), NOT q * (N-1) / N -- the two round + // differently for non-divisible q, and the oracle rounding is consensus. // ----------------------------------------------------------------------- static uint64_t subsidy(uint32_t height) { - static constexpr uint64_t COIN = 100'000'000; // 1 DGB = 10^8 satoshis - - // Phase 1: Pre-DigiShield fixed rewards - if (height < 1440) return 72'000 * COIN; - if (height < 5760) return 16'000 * COIN; - if (height < 67'200) return 8'000 * COIN; - - // Phase 2: -0.5% decay every 10,080 blocks - if (height < 400'000) { - uint64_t base = 8'000 * COIN; - uint32_t periods = (height - 67'200) / 10'080; - // Apply 0.995^periods via integer math - for (uint32_t i = 0; i < periods && base > COIN; ++i) - base = base * 995 / 1000; - return std::max(base, COIN); + static constexpr uint64_t COIN = 1'000'000; // oracle COIN = 1e6 + static constexpr uint32_t nDiffChangeTarget = 67'200; + static constexpr uint32_t alwaysUpdateDiffChangeTarget = 400'000; + static constexpr uint32_t patchBlockRewardDuration = 10'080; + static constexpr uint32_t workComputationChangeTarget = 1'430'000; + static constexpr uint32_t patchBlockRewardDuration2 = 80'160; + + uint64_t nSubsidy = COIN; + if (height < nDiffChangeTarget) { + // Phase 1: pre-DigiShield fixed rewards. + nSubsidy = 8'000 * COIN; + if (height < 1'440) + nSubsidy = 72'000 * COIN; + else if (height < 5'760) + nSubsidy = 16'000 * COIN; + } else if (height < alwaysUpdateDiffChangeTarget) { + // Phase 2: -0.5% per (weeks+1), weeks = blocks / 10080. + uint64_t qSubsidy = 8'000 * COIN; + uint32_t blocks = height - nDiffChangeTarget; + uint32_t weeks = (blocks / patchBlockRewardDuration) + 1; + for (uint32_t i = 0; i < weeks; ++i) + qSubsidy -= qSubsidy / 200; + nSubsidy = qSubsidy; + } else if (height < workComputationChangeTarget) { + // Phase 3: -1% per (weeks+1), weeks = blocks / 80160. + uint64_t qSubsidy = 2'459 * COIN; + uint32_t blocks = height - alwaysUpdateDiffChangeTarget; + uint32_t weeks = (blocks / patchBlockRewardDuration2) + 1; + for (uint32_t i = 0; i < weeks; ++i) + qSubsidy -= qSubsidy / 100; + nSubsidy = qSubsidy; + } else { + // Phase 4: monthly decay (x98884/100000) after work-computation change. + uint64_t qSubsidy = 2'157 * COIN / 2; + uint32_t blocks = height - workComputationChangeTarget; + uint32_t months = static_cast( + static_cast(blocks) * 15 / (3600 * 24 * 365 / 12)); + for (uint32_t i = 0; i < months; ++i) { + qSubsidy *= 98'884; + qSubsidy /= 100'000; + } + nSubsidy = qSubsidy; } - - // Phase 3: -1% decay every 80,160 blocks - uint64_t base = 2'459 * COIN; - uint32_t periods = (height - 400'000) / 80'160; - for (uint32_t i = 0; i < periods && base > COIN; ++i) - base = base * 99 / 100; - return std::max(base, COIN); + if (nSubsidy < COIN) + nSubsidy = COIN; + return nSubsidy; } // GBT_ALGO: multi-algo coin requires specifying which algorithm diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a3310bea5..2137eb704 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,16 @@ if (BUILD_TESTING AND GTest_FOUND) + # DGB block subsidy oracle-conformance (card #156). config_coin.hpp + # subsidy() is header-only/static; no dgb runtime link required. + add_executable(test_dgb_subsidy test_dgb_subsidy.cpp) + target_link_libraries(test_dgb_subsidy PRIVATE + GTest::gtest_main GTest::gtest + btclibs + yaml-cpp::yaml-cpp + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + gtest_discover_tests(test_dgb_subsidy 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_subsidy.cpp b/test/test_dgb_subsidy.cpp new file mode 100644 index 000000000..2ed07605e --- /dev/null +++ b/test/test_dgb_subsidy.cpp @@ -0,0 +1,62 @@ +// Regression test: DGB block subsidy vs the coin's OWN canonical oracle, +// p2pool-dgb-scrypt (bitcoin/networks/digibyte.py :: get_subsidy). +// +// Operator decision, card #156 (2026-06-17): V36 = parity with DGB's own +// reference, so the oracle behaviour IS the spec -- including COIN=1e6 and the +// weeks/months + 1 decay count. These are NOT quirks to "fix". 3-bucket: COMPAT +// (pre-v36 per-coin baseline, temporary for the crossing-soak). +// +// Vectors generated deterministically by executing the oracle get_subsidy() at +// the boundary and interior of every reward phase (all four phases + floor). + +#include +#include + +#include + +namespace { + +struct SubsidyVec { uint32_t height; uint64_t expected; }; + +// Oracle p2pool-dgb-scrypt get_subsidy() reference vectors (satoshis, COIN=1e6). +constexpr SubsidyVec kOracleVectors[] = { + {0, 72000000000ULL}, // phase 1: 72000 DGB + {1, 72000000000ULL}, + {1439, 72000000000ULL}, + {1440, 16000000000ULL}, // phase 1 step -> 16000 + {5759, 16000000000ULL}, + {5760, 8000000000ULL}, // phase 1 step -> 8000 + {67199, 8000000000ULL}, + {67200, 7960000000ULL}, // phase 2 in: weeks+1 = 1 -> -0.5% + {77280, 7920200000ULL}, + {87359, 7920200000ULL}, + {87360, 7880599000ULL}, + {399999, 6746441103ULL}, + {400000, 2434410000ULL}, // phase 3 in: base 2459, weeks+1 = 1 -> -1% + {480159, 2434410000ULL}, + {480160, 2410065900ULL}, + {1429999, 2157824200ULL}, + {1430000, 1078500000ULL}, // phase 4 in: base 2157/2 + {4057999, 921689224ULL}, + {4058000, 911403172ULL}, + {20000000, 331934836ULL}, +}; + +} // namespace + +TEST(DgbSubsidy, MatchesOracleVectors) { + for (const auto& v : kOracleVectors) { + EXPECT_EQ(dgb::CoinParams::subsidy(v.height), v.expected) + << "subsidy(" << v.height << ") diverged from the p2pool-dgb-scrypt oracle"; + } +} + +TEST(DgbSubsidy, NeverBelowCoinFloor) { + // Oracle clamps: if nSubsidy < COIN -> COIN. COIN = 1e6 sat. + // Sweep deep into the monthly-decay region where the reward is floored. + for (uint32_t h = 0; h < 80u; ++h) { + const uint32_t height = 130000000u + h * 1000000u; + EXPECT_GE(dgb::CoinParams::subsidy(height), 1000000ULL) + << "subsidy floor breached at height " << height; + } +}