diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d71bd27f..c7602ad96 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,7 +84,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 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_dc_proof_test dgb_aux_doge_bind_parsers_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_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_naughty_propagation_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_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 v37_test \ -j$(nproc) - name: Run tests @@ -216,7 +216,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 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_dc_proof_test dgb_aux_doge_bind_parsers_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_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_naughty_propagation_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_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 test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/share_weight_decay.hpp b/src/impl/dgb/coin/share_weight_decay.hpp new file mode 100644 index 000000000..5a3941e12 --- /dev/null +++ b/src/impl/dgb/coin/share_weight_decay.hpp @@ -0,0 +1,54 @@ +#pragma once +// V36 PPLNS exponential weight-decay parameters -- SSOT. +// +// Conforms to frstrtr/p2pool-merged-v36 p2pool/data.py +// get_decayed_cumulative_weights (the V36-native PPLNS path): +// half_life = max(net.CHAIN_LENGTH // 4, 1) +// decay_per = SCALE - (SCALE * 693147) // (1_000_000 * half_life) +// decayed_att = (att * decay_fp) >> 40 # 40-bit fixed point +// where SCALE = 1 << 40, 693147 = ln(2) * 1e6 truncated. The fixed-point +// form is consensus-deterministic (no floating point). +// +// V36-NATIVE shared structure (3-bucket rule, bucket 2): the decayed PPLNS +// weighting is the V36 reward-distribution shape, distinct from the older +// p2pool-dgb-scrypt flat WeightsSkipList path (which has no decay and stays +// the live baseline during the crossing window). Standardize cross-coin +// toward this shape for the V37 unified migration. +// +// FENCED: header-only, share_tracker.hpp NOT yet rewired. The three open-coded +// copies of this arithmetic -- +// init_decay_table() (decay table precompute) +// get_v36_decayed_cumulative_weights() (hot-path PPLNS weights) +// the [PARENT-PPLNS] diagnostic dump (GENTX-mismatch debug only) +// -- delegate onto this SSOT in a byte-identity follow-on PR. +#include +#include + +namespace dgb::coin::weight_decay { + +// 40-bit fixed-point scale (1.0 == DECAY_SCALE). Consensus-deterministic. +inline constexpr uint64_t DECAY_PRECISION = 40; +inline constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; +// ln(2) * 1e6, truncated for integer arithmetic. +inline constexpr uint64_t LN2_MICRO = 693147; + +// half_life = max(chain_length / 4, 1) (oracle: max(CHAIN_LENGTH // 4, 1)). +// Guard keeps the divisor non-zero for tiny chains. +inline constexpr uint32_t half_life(uint32_t chain_length) { + return std::max(chain_length / 4u, uint32_t(1)); +} + +// Per-share decay multiplier in fixed point: +// 2^(-1/H) ~= 1 - ln(2)/H -> SCALE - SCALE*693147 / (1e6 * H) +inline constexpr uint64_t decay_per_share(uint32_t chain_length) { + return DECAY_SCALE + - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life(chain_length)); +} + +// Apply the accumulated depth-decay factor to a share attempts count: +// decayed_att = (att * decay_fp) >> 40 +inline constexpr uint64_t decayed_attempts(uint64_t att, uint64_t decay_fp) { + return (att * decay_fp) >> DECAY_PRECISION; +} + +} // namespace dgb::coin::weight_decay diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 020115c25..89e2d9736 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -736,6 +736,18 @@ if (BUILD_TESTING AND GTest_FOUND) GTest::gtest_main GTest::gtest core c2pool_hashrate c2pool_merged_mining c2pool_payout c2pool_storage) gtest_add_tests(dgb_pool_attempts_per_second_test "" AUTO) + # dgb_share_weight_decay_test: FENCED, additive KAT pinning the V36 PPLNS + # exponential weight-decay rate arithmetic (half_life = CHAIN_LENGTH/4, + # decay_per = SCALE - SCALE*693147/(1e6*half_life), decayed_att = + # (att*decay_fp)>>40) lifted into coin/share_weight_decay.hpp vs the + # frstrtr/p2pool-merged-v36 data.py get_decayed_cumulative_weights oracle. + # Header-only constexpr (no core types); share_tracker.hpp NOT yet rewired + # (the 3-copy delegation is the byte-identity follow-on). MUST also be in + # the build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_share_weight_decay_test share_weight_decay_test.cpp) + target_link_libraries(dgb_share_weight_decay_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_share_weight_decay_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/share_weight_decay_test.cpp b/src/impl/dgb/test/share_weight_decay_test.cpp new file mode 100644 index 000000000..188b61675 --- /dev/null +++ b/src/impl/dgb/test/share_weight_decay_test.cpp @@ -0,0 +1,87 @@ +// dgb::coin::weight_decay -- V36 PPLNS exponential weight-decay KAT. +// +// FENCED conformance test (no production code touched). Pins the decay-rate +// arithmetic lifted into coin/share_weight_decay.hpp against the +// frstrtr/p2pool-merged-v36 oracle p2pool/data.py get_decayed_cumulative_weights: +// half_life = max(net.CHAIN_LENGTH // 4, 1) +// decay_per = SCALE - (SCALE * 693147) // (1_000_000 * half_life) +// decayed_att = (att * decay_fp) >> 40 +// SCALE = 1 << 40, 693147 = ln(2) * 1e6 truncated. +// +// All expected values are HAND-DERIVED by independently evaluating the oracle +// Python expression above (run as a separate reference, not by calling the +// helper under test), so the test is NON-CIRCULAR. The CL goldens cover the +// V36 DGB chain length (CHAIN_LENGTH = 24*60*60//10 = 8640), the older +// p2pool-dgb-scrypt baseline (12*60*60//15 = 2880), and the tiny-chain +// half_life=1 guard. +// +// MUST appear in BOTH the ctest registration (this dir CMakeLists.txt) AND the +// build.yml --target allowlist, or it becomes a #143-style NOT_BUILT sentinel +// that reds master. + +#include + +#include + +namespace wd = dgb::coin::weight_decay; + +// ---- fixed-point constants match the oracle exactly --------------------- + +TEST(DgbWeightDecay, FixedPointConstants) { + EXPECT_EQ(wd::DECAY_PRECISION, 40u); + EXPECT_EQ(wd::DECAY_SCALE, uint64_t(1) << 40); + EXPECT_EQ(wd::DECAY_SCALE, uint64_t(1099511627776)); + EXPECT_EQ(wd::LN2_MICRO, 693147u); +} + +// ---- half_life = max(CHAIN_LENGTH // 4, 1) ------------------------------ + +TEST(DgbWeightDecay, HalfLifeMatchesOracle) { + EXPECT_EQ(wd::half_life(8640), 2160u); // V36 DGB: 8640 // 4 + EXPECT_EQ(wd::half_life(2880), 720u); // baseline: 2880 // 4 + EXPECT_EQ(wd::half_life(3600), 900u); + EXPECT_EQ(wd::half_life(720), 180u); +} + +TEST(DgbWeightDecay, HalfLifeTinyChainGuard) { + // chain_length // 4 == 0 must clamp to 1 (oracle max(.., 1)). + EXPECT_EQ(wd::half_life(4), 1u); + EXPECT_EQ(wd::half_life(3), 1u); + EXPECT_EQ(wd::half_life(1), 1u); + EXPECT_EQ(wd::half_life(0), 1u); +} + +// ---- decay_per = SCALE - SCALE*693147 // (1e6 * half_life) --------------- +// Goldens computed independently from the oracle expression. + +TEST(DgbWeightDecay, DecayPerShareMatchesOracle) { + EXPECT_EQ(wd::decay_per_share(8640), uint64_t(1099158792968)); // hl=2160 (V36 DGB) + EXPECT_EQ(wd::decay_per_share(2880), uint64_t(1098453123351)); // hl=720 (baseline) + EXPECT_EQ(wd::decay_per_share(3600), uint64_t(1098664824236)); // hl=900 + EXPECT_EQ(wd::decay_per_share(720), uint64_t(1095277610075)); // hl=180 + EXPECT_EQ(wd::decay_per_share(4), uint64_t(337388441518)); // hl=1 (guard) +} + +// ---- decayed_att = (att * decay_fp) >> 40 ------------------------------- +// The per-depth decay_fp accumulation (decay_fp_{n+1} = decay_fp_n * decay_per) +// is a 80-bit product done with mul128_shift in the caller's walk -- that +// 128-bit step lives in share_tracker.hpp, NOT in this SSOT (which exposes only +// the rate constants + the final attempts shift). So the decay_fp values below +// are supplied as DIRECT oracle literals (computed in Python bignums from +// decay_fp_{n+1} = (decay_fp_n * decay_per) >> 40 with CL=8640, +// decay_per=1099158792968), and decayed_attempts is checked against each. The +// att*decay_fp product fits uint64 here because att (65535) is small. + +TEST(DgbWeightDecay, DecayedAttemptsAtOracleDepthFactors) { + const uint64_t att = 65535; // target_to_average_attempts placeholder + // depth 0..3 decay_fp factors for the V36 DGB chain (CL=8640): + EXPECT_EQ(wd::decayed_attempts(att, uint64_t(1099511627776)), uint64_t(65535)); // depth 0: 1.0 + EXPECT_EQ(wd::decayed_attempts(att, uint64_t(1099158792968)), uint64_t(65513)); // depth 1 + EXPECT_EQ(wd::decayed_attempts(att, uint64_t(1098806071385)), uint64_t(65492)); // depth 2 + EXPECT_EQ(wd::decayed_attempts(att, uint64_t(1098453462991)), uint64_t(65471)); // depth 3 +} + +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 +}