diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 327092e27..0dd18059f 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 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 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 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 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 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 test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/binomial_conf_interval.hpp b/src/impl/dgb/coin/binomial_conf_interval.hpp new file mode 100644 index 000000000..c98e63338 --- /dev/null +++ b/src/impl/dgb/coin/binomial_conf_interval.hpp @@ -0,0 +1,96 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::binomial_conf_interval -- FENCED, additive SSOT lift of the +// p2pool util/math.py:133 binomial_conf_interval (Wilson score interval) and +// its ierf/erf helpers. This is the stale-rate / dead-share CONFIDENCE-INTERVAL +// display arithmetic the status loop renders (main.py:406/425/426 via +// format_binomial_conf). DIAGNOSTICS ONLY -- it touches no PoW, share format, +// sharechain rule, PPLNS math, or block submission, so the p2pool-merged-v36 +// surface is NONE. No call site is rewired by this slice (the byte-identity +// delegation, if ever wanted, is a separate follow-on); this header is the +// dormant SSOT + its KAT. +// +// FAITHFULNESS NOTE: the oracle does NOT use libm erf. It ships its own +// Abramowitz & Stegun 7.1.26 polynomial approximation (util/math.py:100) and +// ierf Newton-iterates ON THAT approximation (10 steps from 0, true-erf +// derivative). Porting std::erf instead would shift ierf and break byte-parity +// with the oracle, so erf_as7126 below reproduces the oracle polynomial exactly. +// +// Per-coin isolation: src/impl/dgb/ only. Pure header (/) -- no +// core/share/tracker translation unit pulled in. +// --------------------------------------------------------------------------- + +#include +#include +#include + +namespace dgb { +namespace coin { + +// Oracle erf -- Abramowitz & Stegun formula 7.1.26 (util/math.py:100). +// erf(-x) = -erf(x) handled via sign capture, exactly as the oracle. +inline double erf_as7126(double x) +{ + int sign = 1; + if (x < 0) sign = -1; + x = std::fabs(x); + + const double a1 = 0.254829592; + const double a2 = -0.284496736; + const double a3 = 1.421413741; + const double a4 = -1.453152027; + const double a5 = 1.061405429; + const double p = 0.3275911; + + const double t = 1.0 / (1.0 + p * x); + const double y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) + * t * std::exp(-x * x); + return sign * y; +} + +// Oracle ierf (util/math.py:130) -- Newton root-find of erf(x) - z, 10 steps +// from guess 0, no bounds. find_root step: guess -= (erf(guess)-z)/erf'(guess), +// with erf'(x) = 2 e^{-x^2} / sqrt(pi) (the TRUE derivative, matching the +// oracle's analytic dy term even though erf itself is the approximation above). +inline double ierf(double z) +{ + double guess = 0.0; + for (int i = 0; i < 10; ++i) { + const double dy = 2.0 * std::exp(-guess * guess) / std::sqrt(M_PI); + const double y_over_dy = (erf_as7126(guess) - z) / dy; + const double prev = guess; + guess = guess - y_over_dy; + if (guess == prev) break; + } + return guess; +} + +// Wilson score interval (util/math.py:133), n > 0 path. Returns {left, right}, +// each clipped to [0,1], with the interval EXTENDED to include the point +// estimate p = x/n (oracle add_to_range, util/math.py:48). PRECONDITION n > 0: +// the oracle's n == 0 branch seeds a random.random() placeholder window of +// width `conf` -- a non-deterministic display sentinel handled by the caller, +// deliberately NOT reproduced here (a pure function cannot mirror it). +inline std::array binomial_conf_interval(double x, double n, + double conf = 0.95) +{ + const double z = std::sqrt(2.0) * ierf(conf); + const double p = x / n; + const double topa = p + z * z / 2.0 / n; + const double topb = z * std::sqrt(p * (1.0 - p) / n + z * z / 4.0 / (n * n)); + const double bottom = 1.0 + z * z / n; + + double lo = (topa - topb) / bottom; + double hi = (topa + topb) / bottom; + + // add_to_range(p, [lo, hi]) = (min(lo, p), max(hi, p)): pull the endpoints + // out to always bracket the point estimate before clipping. + lo = std::min(lo, p); + hi = std::max(hi, p); + + const auto clip01 = [](double v) { return std::max(0.0, std::min(1.0, v)); }; + return { clip01(lo), clip01(hi) }; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index cba3788c8..6e3033c3c 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_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 + # coin/binomial_conf_interval.hpp. DIAGNOSTICS-ONLY stale-rate display math + # (main.py format_binomial_conf), no consensus surface; golden endpoints are + # non-circular Python-oracle output. Pure header (/) -> links + # only GTest. No call site rewired. MUST also be in the build.yml --target + # allowlist (#143 NOT_BUILT trap). + add_executable(dgb_binomial_conf_interval_test binomial_conf_interval_test.cpp) + target_link_libraries(dgb_binomial_conf_interval_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_binomial_conf_interval_test "" AUTO) # dgb_pool_efficiency_test: FENCED, additive KAT pinning the pool # EFFICIENCY / REAL-HASHRATE diagnostics arithmetic in pool_efficiency.hpp vs # the p2pool main.py status-loop oracle (stale_prop = (orphan+doa)/total; diff --git a/src/impl/dgb/test/binomial_conf_interval_test.cpp b/src/impl/dgb/test/binomial_conf_interval_test.cpp new file mode 100644 index 000000000..70c9451b2 --- /dev/null +++ b/src/impl/dgb/test/binomial_conf_interval_test.cpp @@ -0,0 +1,91 @@ +// --------------------------------------------------------------------------- +// dgb_binomial_conf_interval_test -- FENCED, additive KAT pinning the pure +// arithmetic core of the p2pool util/math.py binomial_conf_interval (Wilson +// score interval) lifted into coin/binomial_conf_interval.hpp. The expected +// interval endpoints are NON-CIRCULAR golden values captured by running the +// actual Python oracle (util/math.py:133, with its A&S-7.1.26 erf + 10-step +// Newton ierf) on frstrtr/p2pool-dgb-scrypt -- they are NOT recomputed from +// the C++ port. Diagnostics-only display math; no consensus surface. +// --------------------------------------------------------------------------- + +#include +#include "../coin/binomial_conf_interval.hpp" + +using dgb::coin::binomial_conf_interval; +using dgb::coin::ierf; + +// Tight tolerance: a faithful double-precision port of the same operations in +// the same order should match the oracle to well under 1e-9. +static constexpr double kEps = 1e-9; + +// z = sqrt(2)*ierf(0.95) must reproduce the canonical 1.95996 two-sided 95% +// normal quantile -- a non-circular cross-check the ierf/erf port is correct. +TEST(BinomialConfInterval, IerfZScore95MatchesNormalQuantile) +{ + const double z = std::sqrt(2.0) * ierf(0.95); + EXPECT_NEAR(z, 1.9599628032746732, kEps); +} + +TEST(BinomialConfInterval, IerfZScore6826MatchesOneSigma) +{ + const double z = std::sqrt(2.0) * ierf(0.6826); + EXPECT_NEAR(z, 0.9998151342295566, kEps); +} + +// --- Golden interval endpoints straight from the Python oracle -------------- +TEST(BinomialConfInterval, Oracle_5_of_100_95) +{ + auto r = binomial_conf_interval(5.0, 100.0, 0.95); + EXPECT_NEAR(r[0], 0.021543689574590505, kEps); + EXPECT_NEAR(r[1], 0.111750420163752220, kEps); +} + +// Zero successes: lower endpoint clips to exactly 0, point estimate p=0 is +// inside the (add_to_range-extended) interval. +TEST(BinomialConfInterval, Oracle_0_of_100_95_ClipsLowerToZero) +{ + auto r = binomial_conf_interval(0.0, 100.0, 0.95); + EXPECT_DOUBLE_EQ(r[0], 0.0); + EXPECT_NEAR(r[1], 0.036993455264825250, kEps); +} + +// Symmetric case p=0.5: interval centred on 0.5. +TEST(BinomialConfInterval, Oracle_50_of_100_95_Symmetric) +{ + auto r = binomial_conf_interval(50.0, 100.0, 0.95); + EXPECT_NEAR(r[0], 0.403831586182331560, kEps); + EXPECT_NEAR(r[1], 0.596168413817668400, kEps); + // Symmetry about 0.5 is the structural invariant of p=0.5. + EXPECT_NEAR((r[0] + r[1]) / 2.0, 0.5, kEps); +} + +// Different confidence level (0.6826 ~ 1 sigma) exercises a distinct ierf root. +TEST(BinomialConfInterval, Oracle_10_of_50_6826) +{ + auto r = binomial_conf_interval(10.0, 50.0, 0.6826); + EXPECT_NEAR(r[0], 0.149571297975416520, kEps); + EXPECT_NEAR(r[1], 0.262189143777230850, kEps); +} + +// Small-rate, large-n tail. +TEST(BinomialConfInterval, Oracle_3_of_1000_95) +{ + auto r = binomial_conf_interval(3.0, 1000.0, 0.95); + EXPECT_NEAR(r[0], 0.001020784487127052, kEps); + EXPECT_NEAR(r[1], 0.008783008879983123, kEps); +} + +// --- Structural invariants (non-golden, hold for every valid input) --------- +TEST(BinomialConfInterval, BracketsPointEstimateAndStaysInUnit) +{ + for (double x : {0.0, 1.0, 7.0, 33.0, 99.0}) { + const double n = 100.0; + auto r = binomial_conf_interval(x, n, 0.95); + const double p = x / n; + EXPECT_GE(p + kEps, r[0]) << "left must not exceed point estimate, x=" << x; + EXPECT_LE(p - kEps, r[1]) << "right must not be below point estimate, x=" << x; + EXPECT_GE(r[0], 0.0); + EXPECT_LE(r[1], 1.0); + EXPECT_LE(r[0], r[1]); + } +}