From f376d687d83522ef99d4b4adcd49101aac54f11b Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sat, 27 Jun 2026 00:12:21 +0000 Subject: [PATCH] dgb: saturating uint288 emergency-decay shift + proving KAT Replace uint256 eased<<=halvings (wraps at 2^256, ~50-68min death-spiral gap vs oracle) with a saturating shift computed in uint288 so emergency decay saturates to MAX, matching frstrtr/p2pool-dgb-scrypt. Adds EmergencyDecaySaturation KAT: h21=57.5min fails-before/passes-after, NEW==ORACLE 0-mismatch + interpolation, death-spiral>0. Consensus-bearing; held for operator scope tap, no self-merge. --- .github/workflows/build.yml | 4 +- src/impl/dgb/share_tracker.hpp | 24 +- src/impl/dgb/test/CMakeLists.txt | 15 ++ .../test/emergency_decay_saturation_test.cpp | 230 ++++++++++++++++++ 4 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 src/impl/dgb/test/emergency_decay_saturation_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 588f24b20..71deda04a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,7 +94,7 @@ jobs: dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test nmc_mempool_name_test nmc_block_broadcast_test nmc_host_dualpath_test nmc_fallback_path_conformance_test dgb_gentx_share_path_test dgb_conn_pplns_producer_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test dgb_aux_doge_mm_commitment_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_test dgb_compact_blocks_bip152_parity_test dgb_aux_dual_target_select_test dgb_aux_broadcast_path_election_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ - dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test dgb_share_target_genesis_test dgb_share_bits_oracle_pin_test dgb_pool_msg_wire_test dgb_get_shares_walk_test dgb_download_stops_test dgb_think_p1_walk_bounds_test dgb_think_p1_desired_emit_test dgb_think_p6_desired_cutoff_test dgb_think_p4_head_keys_test dgb_think_p3_best_head_test dgb_g1_oracle_byte_parity_test dgb_think_p2_walk_bounds_test dgb_expected_time_to_block_test dgb_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_test dgb_get_height_and_last_endpoints_test dgb_chain_walk_window_test dgb_redistribute_delegate_ghal_test dgb_share_weight_decay_test dgb_naughty_propagation_test dgb_hash_format_parity_test v37_test \ + dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test dgb_share_target_genesis_test dgb_share_bits_oracle_pin_test dgb_pool_msg_wire_test dgb_get_shares_walk_test dgb_download_stops_test dgb_think_p1_walk_bounds_test dgb_think_p1_desired_emit_test dgb_think_p6_desired_cutoff_test dgb_think_p4_head_keys_test dgb_think_p3_best_head_test dgb_g1_oracle_byte_parity_test dgb_think_p2_walk_bounds_test dgb_expected_time_to_block_test dgb_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_test dgb_get_height_and_last_endpoints_test dgb_chain_walk_window_test dgb_redistribute_delegate_ghal_test dgb_share_weight_decay_test dgb_naughty_propagation_test dgb_hash_format_parity_test dgb_emergency_decay_saturation_test v37_test \ -j8 - name: Run tests @@ -237,7 +237,7 @@ jobs: dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test nmc_mempool_name_test nmc_block_broadcast_test nmc_host_dualpath_test nmc_fallback_path_conformance_test dgb_gentx_share_path_test dgb_conn_pplns_producer_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test dgb_aux_doge_mm_commitment_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_test dgb_compact_blocks_bip152_parity_test dgb_aux_dual_target_select_test dgb_aux_broadcast_path_election_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ - dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test dgb_share_target_genesis_test dgb_share_bits_oracle_pin_test dgb_pool_msg_wire_test dgb_get_shares_walk_test dgb_download_stops_test dgb_think_p1_walk_bounds_test dgb_think_p1_desired_emit_test dgb_think_p6_desired_cutoff_test dgb_think_p4_head_keys_test dgb_think_p3_best_head_test dgb_g1_oracle_byte_parity_test dgb_think_p2_walk_bounds_test dgb_expected_time_to_block_test dgb_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_test dgb_get_height_and_last_endpoints_test dgb_chain_walk_window_test dgb_redistribute_delegate_ghal_test dgb_share_weight_decay_test dgb_naughty_propagation_test dgb_hash_format_parity_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ + dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test dgb_share_target_genesis_test dgb_share_bits_oracle_pin_test dgb_pool_msg_wire_test dgb_get_shares_walk_test dgb_download_stops_test dgb_think_p1_walk_bounds_test dgb_think_p1_desired_emit_test dgb_think_p6_desired_cutoff_test dgb_think_p4_head_keys_test dgb_think_p3_best_head_test dgb_g1_oracle_byte_parity_test dgb_think_p2_walk_bounds_test dgb_expected_time_to_block_test dgb_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_test dgb_get_height_and_last_endpoints_test dgb_chain_walk_window_test dgb_redistribute_delegate_ghal_test dgb_share_weight_decay_test dgb_naughty_propagation_test dgb_hash_format_parity_test dgb_emergency_decay_saturation_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j8 diff --git a/src/impl/dgb/share_tracker.hpp b/src/impl/dgb/share_tracker.hpp index 2e5ebebd9..087d52c79 100644 --- a/src/impl/dgb/share_tracker.hpp +++ b/src/impl/dgb/share_tracker.hpp @@ -1528,19 +1528,23 @@ class ShareTracker auto excess = time_since_share - emergency_threshold; auto halvings = excess / half_life; auto remainder = excess % half_life; - // 2^halvings with linear interpolation for fractional part - uint256 eased = prev_max_target; - if (halvings < 256) - eased <<= halvings; - else - eased = MAX_TARGET; - // Linear interpolation: eased = eased * (half_life + remainder) / half_life + // Step 3 (emergency ease): 2^halvings with linear interpolation. + // Shift in the wider uint288 accumulator, saturating at + // MAX_TARGET as we go so no uint256- OR uint288-representable + // wrap can occur. The old uint256 `eased <<= halvings` wrapped + // mod 2^256, collapsing the eased target to a hard value and + // stalling the DAA stall-path (~50-68min death-spiral hole). uint288 eased_288; - eased_288.SetHex(eased.GetHex()); - eased_288 = eased_288 * static_cast(half_life + remainder); - eased_288 = eased_288 / static_cast(half_life); + eased_288.SetHex(prev_max_target.GetHex()); uint288 max_288; max_288.SetHex(MAX_TARGET.GetHex()); + for (uint64_t i = 0; i < static_cast(halvings) && i < 288 && eased_288 <= max_288; ++i) + eased_288 <<= 1; + if (eased_288 > max_288) + eased_288 = max_288; + // Linear interpolation: eased = eased * (half_life + remainder) / half_life + eased_288 = eased_288 * static_cast(half_life + remainder); + eased_288 = eased_288 / static_cast(half_life); if (eased_288 > max_288) clamp_ref_target = MAX_TARGET; else diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 8cbf590c5..b132e7a27 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -826,6 +826,21 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(dgb_share_weight_decay_test PRIVATE GTest::gtest_main GTest::gtest) gtest_add_tests(dgb_share_weight_decay_test "" AUTO) + # dgb_emergency_decay_saturation_test: FENCED, additive KAT pinning the + # share_tracker.hpp Step-3 emergency time-based decay (death-spiral + # prevention) eased target == min(prev_max_target * 2^halvings, MAX_TARGET) + # against an arbitrary-precision oracle (right-shift threshold form, no wrap + # at 2^256 OR 2^288). Proves (a) the load-bearing h=21/57.5min fails-now/ + # passes-after boundary vector and (b) full-sweep NEW==ORACLE identity. Uses + # core uint256/uint288 -> links core only (no OBJECT lib). MUST also be in + # the build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_emergency_decay_saturation_test emergency_decay_saturation_test.cpp) + target_link_libraries(dgb_emergency_decay_saturation_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_emergency_decay_saturation_test "" AUTO) # dgb_binomial_conf_interval_test: FENCED, additive KAT pinning the pure # Wilson-score confidence-interval arithmetic of p2pool util/math.py:133 # binomial_conf_interval (+ ierf/erf A&S-7.1.26 helpers) lifted into diff --git a/src/impl/dgb/test/emergency_decay_saturation_test.cpp b/src/impl/dgb/test/emergency_decay_saturation_test.cpp new file mode 100644 index 000000000..442efbe2b --- /dev/null +++ b/src/impl/dgb/test/emergency_decay_saturation_test.cpp @@ -0,0 +1,230 @@ +// --------------------------------------------------------------------------- +// dgb emergency-decay (Step 3) saturating-shift regression guard. +// +// Pins the DGB sharechain emergency time-based decay ("death spiral +// prevention") at share_tracker.hpp Step 3 against its oracle. The eased +// target doubles once per half_life past the emergency threshold; the +// canonical retarget is the arbitrary-precision saturating value +// +// eased = min(prev_max_target * 2^halvings, MAX_TARGET) +// +// with NO wrap at any bound. The shipped fix performs the shift incrementally +// in the wider uint288 accumulator, saturating to MAX_TARGET as it goes, so +// neither a uint256 wrap (at 2^256) NOR a single-shot uint288 wrap (at 2^288, +// reachable for halvings >= 53) can collapse the eased target. +// +// The pre-fix shape did `uint256 eased <<= halvings`, which truncates mod +// 2^256: a prev target whose mantissa shifts past bit 256 wraps back to a +// SUB-MAX value, so the DAA stops easing and the chain death-spirals. The +// observed stall hole sat at ~50-68 min since last share. +// +// ORACLE NON-CIRCULARITY: oracle() below never shifts a value that could +// overflow -- it tests prev <= (MAX >> halvings) FIRST (a right-shift, which +// cannot overflow) and only then performs the in-range left-shift. A proxy +// that instead did `uint288 << halvings` would itself wrap at 2^288 and +// fabricate false mismatches; this guard MUST keep the right-shift threshold +// form. (Proxy gotcha baked in per the boundary-divergence evidence, +// sha256 5d5c9452..., ~/dgb-emergency-decay-evidence/.) +// +// Links ONLY core (uint256/uint288 big-int) + gtest -- no dgb OBJECT lib / +// transport, matching the pure-math scope of the Step-3 computation. +// --------------------------------------------------------------------------- + +#include +#include +#include + +#include + +#include + +namespace { + +// DGB Scrypt MAX_TARGET (powLimit), bits_to_target upper bound. +uint256 MAXT() +{ + uint256 t; + t.SetHex("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + return t; +} + +uint256 H(const std::string& s) +{ + uint256 t; + t.SetHex(s); + return t; +} + +// TRUE oracle: arbitrary-precision min(prev * 2^h, MAX). No wrap at any bound. +// prev<> h); the right-shift can never overflow, so +// the threshold test is exact and the surviving left-shift is in-range. +uint256 oracle(const uint256& prev, uint32_t h) +{ + uint256 mx = MAXT(); + if (h >= 256) + return prev.IsNull() ? prev : mx; + uint256 thr = mx; + thr >>= h; + if (prev > thr) + return mx; + uint288 f; + f.SetHex(prev.GetHex()); + f <<= h; + uint256 r; + r.SetHex(f.GetHex()); + return r; +} + +// NEW shape: byte-for-byte the share_tracker.hpp Step-3 emergency-ease body +// (incremental uint288 saturating shift + linear interpolation), lifted as a +// pure function so the consensus math is provable in isolation. +uint256 new_shape(const uint256& prev, uint32_t h, uint32_t HL, uint32_t rem) +{ + uint288 x; + x.SetHex(prev.GetHex()); + uint288 m; + m.SetHex(MAXT().GetHex()); + for (uint64_t i = 0; i < static_cast(h) && i < 288 && x <= m; ++i) + x <<= 1; + if (x > m) + x = m; + x = x * static_cast(HL + rem); + x = x / static_cast(HL); + if (x > m) + return MAXT(); + uint256 r; + r.SetHex(x.GetHex()); + return r; +} + +// OLD shape: the pre-fix uint256 `eased <<= halvings`, kept ONLY to demonstrate +// the fails-now divergence the fix repairs. Never shipped past the fix. +uint256 old_shape(const uint256& prev, uint32_t h, uint32_t HL, uint32_t rem) +{ + uint256 e = prev; + if (h < 256) + e <<= h; + else + e = MAXT(); + uint288 x; + x.SetHex(e.GetHex()); + uint288 m; + m.SetHex(MAXT().GetHex()); + x = x * static_cast(HL + rem); + x = x / static_cast(HL); + if (x > m) + return MAXT(); + uint256 r; + r.SetHex(x.GetHex()); + return r; +} + +constexpr uint32_t HL = 150; // half_life = SHARE_PERIOD(15) * 10 + +// 5 representative prev_max_target vectors: a sparse hi-bit + lo-bit pattern +// that wraps under uint256 truncation, a mid-density, two dense, and the unit. +const char* const PREVS[] = { + "0000080000000000000000000000000000000000000000000000000000000001", // bit235 + bit0 + "0000000000000fa1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071829", + "00000000000000000ddccbbaa9988776655443322110ffeeddccbbaa99887766", + "00000000003fedcba9876543210fedcba9876543210fedcba9876543210fedcb", + "0000000000000000000000000000000000000000000000000000000000000001", +}; + +// minutes-since-last-share for a given halving count h: +// threshold = SHARE_PERIOD*20 = 300s, half_life = SHARE_PERIOD*10 = 150s +// t = (300 + h*HL) / 60 minutes +double t_minutes(uint32_t h) { return (300.0 + static_cast(h) * HL) / 60.0; } + +} // namespace + +// (a) LOAD-BEARING fails-now / passes-after vector. +// prev = bit235 + bit0, h = 21 (t = 57.5 min, dead-center of the observed +// 50-68 min stall hole). The pre-fix uint256 shift wraps bit235<<21 -> bit256 +// -> 0 (mod 2^256), collapsing the eased target FAR below the saturating +// oracle; the shipped saturating shift returns MAX_TARGET == oracle. +TEST(EmergencyDecaySaturation, LoadBearingBoundaryVector_h21_57min) +{ + const uint256 prev = H(PREVS[0]); // bit235 + bit0 + const uint32_t h = 21; + ASSERT_DOUBLE_EQ(57.5, t_minutes(h)); // dead-center of the stall hole + + const uint256 want = oracle(prev, h); + EXPECT_EQ(want, MAXT()) << "prev<<21 overflows MAX -> oracle saturates"; + + // fails-now: the pre-fix wrapping shape DIVERGES from the oracle here. + EXPECT_NE(old_shape(prev, h, HL, 0), want) + << "pre-fix uint256 <<= wraps mod 2^256 and death-spirals"; + + // passes-after: the shipped saturating shift MATCHES the oracle. + EXPECT_EQ(new_shape(prev, h, HL, 0), want) + << "saturating uint288 shift == min(prev*2^h, MAX)"; +} + +// (b) Full-sweep NEW == ORACLE behavioral identity, exact boundary (rem == 0), +// every prev vector x every halving count 0..255. Zero mismatch proves the fix +// is the oracle, not merely "closer". +TEST(EmergencyDecaySaturation, NewEqualsOracleIdentity_rem0_sweep) +{ + int mismatch = 0; + for (const char* hx : PREVS) + { + const uint256 prev = H(hx); + for (uint32_t h = 0; h <= 255; ++h) + if (new_shape(prev, h, HL, 0) != oracle(prev, h)) + ++mismatch; + } + EXPECT_EQ(0, mismatch) << "NEW must equal oracle across full h-sweep"; +} + +// (b') Interpolated identity incl. the fractional remainder term: +// new == min( min(prev*2^h, MAX) * (HL+rem)/HL , MAX ) +// across prev x h0..60 x rem. Pins the linear-interpolation tail too. +TEST(EmergencyDecaySaturation, NewEqualsOracleIdentity_interpolated_sweep) +{ + int mismatch = 0; + for (const char* hx : PREVS) + { + const uint256 prev = H(hx); + for (uint32_t h = 0; h <= 60; ++h) + for (uint32_t rem = 0; rem < HL; rem += 29) + { + uint256 base = oracle(prev, h); + uint288 b; + b.SetHex(base.GetHex()); + uint288 m; + m.SetHex(MAXT().GetHex()); + b = b * static_cast(HL + rem); + b = b / static_cast(HL); + uint256 want; + if (b > m) + want = MAXT(); + else + want.SetHex(b.GetHex()); + if (new_shape(prev, h, HL, rem) != want) + ++mismatch; + } + } + EXPECT_EQ(0, mismatch) << "interpolated NEW must equal oracle"; +} + +// (c) Divergence catalogue: there EXISTS a non-empty set of death-spiral cases +// where the pre-fix shape is wrong and the fix is right. Guards against a +// future "simplification" that silently reintroduces the wrap. +TEST(EmergencyDecaySaturation, OldShapeDeathSpiralCasesExist) +{ + int total = 0; + for (const char* hx : PREVS) + { + const uint256 prev = H(hx); + for (uint32_t h = 0; h <= 255; ++h) + { + const uint256 o = oracle(prev, h); + const bool old_ok = (old_shape(prev, h, HL, 0) == o); + const bool new_ok = (new_shape(prev, h, HL, 0) == o); + if (!old_ok && new_ok) + ++total; + } + } + EXPECT_GT(total, 0) << "the wrap bug must be demonstrably reachable"; +}