diff --git a/src/impl/dgb/share_tracker.hpp b/src/impl/dgb/share_tracker.hpp index f404945ef..cb055b9b9 100644 --- a/src/impl/dgb/share_tracker.hpp +++ b/src/impl/dgb/share_tracker.hpp @@ -27,6 +27,7 @@ inline uint64_t mul128_shift(uint64_t a, uint64_t b, unsigned shift) { #include "think_p1_walk_bounds.hpp" // SSOT: think_p1_walk_count / think_p1_in_pruning_zone #include "coin/naughty_propagation.hpp" // SSOT: propagate_naughty_from_parent (data.py:543-549) #include // SSOT: accumulate_version_weights / accumulate_version_counts (#429 follow-on) +#include // SSOT: weight-decay constants/half_life/decay_per_share (#450 rewire) #include "coin/pool_attempts_per_second.hpp" // SSOT: compute_pool_attempts_per_second (#407 follow-on) #include "coin/tail_score_endpoints.hpp" // SSOT: score_* endpoint arithmetic (ShareTracker::score chain-walk, #411 follow-on) #include "coin/chain_walk_window.hpp" // SSOT: lookbehind clamp (#423 diag follow-on); consensus get_desired_version_weights stays inline @@ -159,9 +160,9 @@ struct PPLNSEntry { class DensePPLNSWindow { public: - static constexpr uint64_t DECAY_PRECISION = 40; - static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; - static constexpr uint64_t LN2_MICRO = 693147; + static constexpr uint64_t DECAY_PRECISION = dgb::coin::weight_decay::DECAY_PRECISION; + static constexpr uint64_t DECAY_SCALE = dgb::coin::weight_decay::DECAY_SCALE; + static constexpr uint64_t LN2_MICRO = dgb::coin::weight_decay::LN2_MICRO; // Precomputed decay table: decay_table[d] = iterative decay factor at depth d. // Computed once, reused for every PPLNS walk. Thread-safe after init. @@ -172,8 +173,7 @@ class DensePPLNSWindow { static void init_decay_table(uint32_t chain_len) { if (s_table_initialized && s_decay_table.size() == chain_len) return; - uint32_t half_life = std::max(chain_len / 4, uint32_t(1)); - s_decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); + s_decay_per = dgb::coin::weight_decay::decay_per_share(chain_len); s_decay_table.resize(chain_len); s_decay_table[0] = DECAY_SCALE; for (uint32_t d = 1; d < chain_len; ++d) @@ -1729,12 +1729,8 @@ class ShareTracker && m_decayed_cache_desired == desired_weight) return m_decayed_cache_result; - static constexpr uint64_t DECAY_PRECISION = 40; - static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; - static constexpr uint64_t LN2_MICRO = 693147; - - uint32_t half_life = std::max(PoolConfig::chain_length() / 4, uint32_t(1)); - uint64_t decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); + using namespace dgb::coin::weight_decay; // SSOT decay constants + helpers (#450) + uint64_t decay_per = decay_per_share(PoolConfig::chain_length()); CumulativeWeights result; int32_t share_count = 0; @@ -1804,12 +1800,9 @@ class ShareTracker return; } - static constexpr uint64_t DECAY_PRECISION = 40; - static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; - static constexpr uint64_t LN2_MICRO = 693147; - - uint32_t half_life = std::max(PoolConfig::chain_length() / 4, uint32_t(1)); - uint64_t decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); + using namespace dgb::coin::weight_decay; // SSOT decay constants + helpers (#450) + uint32_t half_life = dgb::coin::weight_decay::half_life(PoolConfig::chain_length()); + uint64_t decay_per = decay_per_share(PoolConfig::chain_length()); int32_t share_count = 0; uint64_t decay_fp = DECAY_SCALE; diff --git a/src/impl/dgb/test/share_weight_decay_test.cpp b/src/impl/dgb/test/share_weight_decay_test.cpp index 188b61675..a4942439c 100644 --- a/src/impl/dgb/test/share_weight_decay_test.cpp +++ b/src/impl/dgb/test/share_weight_decay_test.cpp @@ -85,3 +85,88 @@ TEST(DgbWeightDecay, DecayedAttemptsZeroAndIdentity) { EXPECT_EQ(wd::decayed_attempts(0, wd::DECAY_SCALE), uint64_t(0)); EXPECT_EQ(wd::decayed_attempts(12345, wd::DECAY_SCALE), uint64_t(12345)); // x1.0 } + +// ---- #450 consumer-rewire byte-identity invariance ---------------------- +// +// Pins the share_tracker.hpp rewire (init_decay_table / +// get_v36_decayed_cumulative_weights / dump_v36_pplns_walk) onto this SSOT. +// The OLD path below recomputes decay_per and the iterative decay table with +// the open-coded literals (1<<40, 693147, 1e6, max(CL/4,1)) exactly as the +// three sites did before #450; the SSOT path sources the rate from +// wd::decay_per_share / wd::DECAY_SCALE / wd::DECAY_PRECISION. Both run the +// SAME unsigned __int128 mul-shift used by the production mul128_shift / +// uint288 hot path, so this proves the rewire is value-invariant +// (byte-for-byte) across the full decayed-cumulative-weight vector. The OLD +// path is independent of the helper under test -> NON-CIRCULAR. + +TEST(DgbWeightDecay, DecayedCumulativeWeightVectorInvariance) { + using uint128 = unsigned __int128; + const uint32_t CL = 8640; // V36 DGB chain length + + // --- OLD open-coded decay_per (literals, pre-#450) --- + const uint64_t old_half_life = std::max(CL / 4u, 1u); + const uint64_t old_decay_per = + (uint64_t(1) << 40) + - ((uint64_t(1) << 40) * 693147ull) / (1000000ull * old_half_life); + + // --- SSOT decay_per --- + const uint64_t ssot_decay_per = wd::decay_per_share(CL); + + ASSERT_EQ(ssot_decay_per, old_decay_per); + + // Synthetic share window: distinct attempts + donation per depth. + const std::vector att = { + 1000ull, 999983ull, 2500000ull, 65535ull, + 1ull, 123456789ull, 42ull, 7777777ull, + }; + const std::vector don = { 0, 100, 32767, 65535, 1, 12345, 200, 65534 }; + const size_t N = att.size(); + + // --- OLD path: iterative decay table via uint128 mul-shift --- + std::vector old_table(N); + old_table[0] = (uint64_t(1) << 40); // DECAY_SCALE + for (size_t d = 1; d < N; ++d) + old_table[d] = + (uint64_t)((uint128(old_table[d-1]) * old_decay_per) >> 40); + + // --- SSOT path: same uint128 mul-shift, SSOT-sourced constants/rate --- + std::vector ssot_table(N); + ssot_table[0] = wd::DECAY_SCALE; + for (size_t d = 1; d < N; ++d) + ssot_table[d] = (uint64_t)( + (uint128(ssot_table[d-1]) * ssot_decay_per) >> wd::DECAY_PRECISION); + + // Decay-table vectors must be element-by-element identical. + ASSERT_EQ(old_table.size(), ssot_table.size()); + for (size_t d = 0; d < N; ++d) + EXPECT_EQ(old_table[d], ssot_table[d]) << "decay_table depth " << d; + + // Per-depth decayed attempts + addr/donation split + running cumulative + // totals, mirroring the consumer's uint288 hot path (replicated with + // uint128 here; the SSOT scalar decayed_attempts is uint64-only so it is + // intentionally NOT used for the att*table widening). + uint128 old_cum_total = 0, old_cum_don = 0; + uint128 ssot_cum_total = 0, ssot_cum_don = 0; + for (size_t d = 0; d < N; ++d) { + const uint128 old_dec = (uint128(att[d]) * old_table[d]) >> 40; + const uint128 ssot_dec = + (uint128(att[d]) * ssot_table[d]) >> wd::DECAY_PRECISION; + EXPECT_EQ((uint64_t)old_dec, (uint64_t)ssot_dec) << "decayed_att depth " << d; + + const uint128 old_addr = old_dec * uint128(65535 - don[d]); + const uint128 old_donw = old_dec * uint128(don[d]); + const uint128 ssot_addr = ssot_dec * uint128(65535 - don[d]); + const uint128 ssot_donw = ssot_dec * uint128(don[d]); + EXPECT_EQ((uint64_t)old_addr, (uint64_t)ssot_addr) << "addr_w depth " << d; + EXPECT_EQ((uint64_t)old_donw, (uint64_t)ssot_donw) << "don_w depth " << d; + + old_cum_total += old_addr + old_donw; + old_cum_don += old_donw; + ssot_cum_total += ssot_addr + ssot_donw; + ssot_cum_don += ssot_donw; + EXPECT_EQ((uint64_t)old_cum_total, (uint64_t)ssot_cum_total) + << "cum_total depth " << d; + EXPECT_EQ((uint64_t)old_cum_don, (uint64_t)ssot_cum_don) + << "cum_don depth " << d; + } +}