From 19f9fb718cf88067fc92f3b75fde9e75703809a9 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Tue, 23 Jun 2026 21:55:59 +0000 Subject: [PATCH] dgb(Phase-B): SSOT expected-time-to-block diagnostic + non-circular KAT Lift the final expected-time-to-block arithmetic (etb_secs = average_attempts / real_pool_hs, with the uint64-overflow 1e18 sentinel) out of node.cpp diagnostics into a header-only SSOT, expected_time_to_block.hpp, mirroring the pool_efficiency.hpp lift. The bits->target->average_attempts conversion stays at the call site on the existing chain SSOTs; this captures only the final combine + sentinel. FENCED: dgb/ only, header-only (), consensus-neutral pure arithmetic. node.cpp NOT yet rewired (the byte-identity delegation that folds real-hashrate + get_stale_counts + this is the follow-on). KAT is non-circular (oracle expression recomputed independently vs the p2pool main.py status loop). 7/7 green. Registered in the dgb test CMakeLists and BOTH build.yml --target allowlist arms. --- .github/workflows/build.yml | 4 +- src/impl/dgb/expected_time_to_block.hpp | 68 +++++++++++++ src/impl/dgb/test/CMakeLists.txt | 12 +++ .../dgb/test/expected_time_to_block_test.cpp | 95 +++++++++++++++++++ 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/expected_time_to_block.hpp create mode 100644 src/impl/dgb/test/expected_time_to_block_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f00e0d1c..fc1cd08c9 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 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 \ 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 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 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 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 \ 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 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 test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/expected_time_to_block.hpp b/src/impl/dgb/expected_time_to_block.hpp new file mode 100644 index 000000000..a917c6d40 --- /dev/null +++ b/src/impl/dgb/expected_time_to_block.hpp @@ -0,0 +1,68 @@ +#pragma once + +// SSOT for the DGB EXPECTED-TIME-TO-BLOCK diagnostic — the final pure-arithmetic +// step the status loop applies when reporting how long the pool is expected to +// take to find a block: +// etb_secs = average_attempts / real_pool_hs +// where average_attempts is the block-target's average attempts (the low 64 bits +// of chain::target_to_average_attempts(bits_to_target(block_bits))) and +// real_pool_hs is the efficiency-adjusted pool hashrate from pool_efficiency.hpp. +// +// This is currently OPEN-CODED inline in node.cpp's diagnostics loop (the +// `etb_secs = block_aps.GetLow64() / real_pool_hs` division plus the +// near-overflow sentinel) feeding the "Pool: ... Expected time to +// block: " line. A silent drift — a flipped numerator/denominator or +// dropping the overflow sentinel — would misreport a node's expected time to +// block with NO compile error, which is operator-facing behavior the V36 +// master-compat invariant pins to the p2pool reference. Lifting the arithmetic +// to a single header-only SSOT lets a KAT pin it exactly against the oracle. +// +// Oracle: p2pool-dgb-scrypt main.py status loop: +// 'Expected time to block: %s' % format_dt( +// 2**256 / current_work.value['bits'].target / real_att_s) +// i.e. average_attempts(target) / real_att_s, where the c2pool path computes +// average_attempts via the existing chain::target_to_average_attempts SSOT and +// guards the case where that count overflows uint64 while the target is +// non-null (reporting a 1e18 sentinel rather than a meaningless small value). +// +// Per-coin isolation: dgb/ only. Header-only, additive; this slice does NOT yet +// rewire node.cpp (that is the byte-identity-fenced delegation follow-on that +// also folds in get_stale_counts + pool_efficiency). It pins the math as a free +// function so the KAT exercises it with no NodeImpl / ShareTracker standup. The +// bits->target->average_attempts conversion stays at the call site on the +// already-verified chain SSOTs; this function captures only the final combine + +// sentinel. Consensus-neutral: pure arithmetic, no value semantics changed. + +#include + +namespace dgb { + +// Expected seconds for the pool to find a block. +// etb_secs = average_attempts / real_pool_hs +// +// average_attempts : static_cast(block_aps.GetLow64()), the low 64 +// bits of the block target's average-attempts count. +// real_pool_hs : efficiency-adjusted pool hashrate (hashes/s). +// average_attempts_overflowed : block_aps.IsNull() — the average-attempts count +// does not fit in uint64 (its low64 is meaningless). +// block_target_nonzero : !block_target.IsNull() — the block target is set. +// +// With no measured hashrate (real_pool_hs <= 0) there is nothing to divide by, so +// the caller never enters this path and etb stays 0; the same guard here keeps +// the function standalone-safe (never divides by zero). When the average-attempts +// count overflows uint64 but the target is genuinely non-null, the low64 division +// would yield a bogus tiny number, so a 1e18 sentinel is reported instead — +// matching the inline guard in node.cpp's diagnostics loop. +inline double compute_expected_time_to_block(double average_attempts, + double real_pool_hs, + bool average_attempts_overflowed, + bool block_target_nonzero) { + if (real_pool_hs <= 0.0) + return 0.0; + double etb_secs = average_attempts / real_pool_hs; + if (average_attempts_overflowed && block_target_nonzero) + etb_secs = 1e18; + return etb_secs; +} + +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 73006095b..e2b2c3cd7 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -660,6 +660,18 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_think_p6_desired_cutoff_test "" AUTO) + # dgb_expected_time_to_block_test: FENCED, additive KAT pinning the + # EXPECTED-TIME-TO-BLOCK diagnostic arithmetic in expected_time_to_block.hpp + # vs the p2pool main.py status-loop oracle (average_attempts(target) / + # real_att_s, with the uint64-overflow 1e18 sentinel). Pure header + # ( only) -> links only GTest. node.cpp NOT yet rewired (delegation + # is the byte-identity follow-on). MUST also be in the build.yml --target + # allowlist (#143 NOT_BUILT trap). + add_executable(dgb_expected_time_to_block_test expected_time_to_block_test.cpp) + target_link_libraries(dgb_expected_time_to_block_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_expected_time_to_block_test "" AUTO) + # dgb_think_p4_head_keys_test: FENCED, additive KAT pinning the think() # Phase-4 head-score KEY construction in think_p4_head_keys.hpp vs the # p2pool data.py think() oracle (selection key (adjusted_work,-reason,-ts) diff --git a/src/impl/dgb/test/expected_time_to_block_test.cpp b/src/impl/dgb/test/expected_time_to_block_test.cpp new file mode 100644 index 000000000..b25eae17c --- /dev/null +++ b/src/impl/dgb/test/expected_time_to_block_test.cpp @@ -0,0 +1,95 @@ +// dgb::compute_expected_time_to_block — expected-time-to-block KAT. +// +// FENCED conformance test (no production code touched). Pins the +// EXPECTED-TIME-TO-BLOCK diagnostic arithmetic lifted into +// expected_time_to_block.hpp against the p2pool-dgb-scrypt oracle main.py status +// loop: +// 'Expected time to block: %s' % format_dt( +// 2**256 / current_work.value['bits'].target / real_att_s) +// == average_attempts(target) / real_att_s, with the uint64-overflow sentinel. +// +// The expected values here are HAND-DERIVED from the oracle formula (not produced +// by calling the helper under test), so the test is non-circular: it +// independently recomputes the oracle expression and asserts the helper matches. +// +// 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 { + +// ---- core division ------------------------------------------------------ + +TEST(DgbExpectedTimeToBlock, EtbIsAverageAttemptsOverRealHashrate) { + // Oracle: average_attempts / real_pool_hs. Recompute independently. + const double avg = 1.0e12; + const double real_hs = 1.0e6; + const double oracle = avg / real_hs; // 1e6 seconds (hand-derived) + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(avg, real_hs, false, true), oracle); + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(avg, real_hs, false, true), 1.0e6); +} + +TEST(DgbExpectedTimeToBlock, FasterPoolHasLowerEtb) { + const double avg = 1.0e12; + // 10x the hashrate -> 1/10th the expected time. + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(avg, 1.0e7, false, true), 1.0e5); +} + +// ---- no-measured-hashrate guard ---------------------------------------- + +TEST(DgbExpectedTimeToBlock, NoHashrateIsZeroNotDivByZero) { + // real_pool_hs <= 0 -> nothing to divide by -> 0.0, never inf/NaN. + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(1.0e12, 0.0, false, true), 0.0); + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(1.0e12, -5.0, false, true), 0.0); +} + +// ---- overflow sentinel -------------------------------------------------- + +TEST(DgbExpectedTimeToBlock, OverflowWithNonNullTargetReportsSentinel) { + // average-attempts count does not fit in uint64 AND target is non-null: + // the low64 division is meaningless -> 1e18 sentinel. + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(123.0, 1.0e6, true, true), 1.0e18); +} + +TEST(DgbExpectedTimeToBlock, OverflowWithNullTargetDoesNotTriggerSentinel) { + // block_target_nonzero == false -> sentinel suppressed, plain division stands. + const double avg = 2.0e9; + const double real_hs = 1.0e3; + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(avg, real_hs, true, false), + avg / real_hs); // 2e6, hand-derived +} + +TEST(DgbExpectedTimeToBlock, NoOverflowIgnoresSentinelRegardlessOfTarget) { + // average_attempts_overflowed == false -> sentinel never applies. + const double avg = 4.0e9; + const double real_hs = 2.0e3; + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(avg, real_hs, false, true), + avg / real_hs); // 2e6 +} + +// ---- end-to-end chain mirroring node.cpp diagnostics -------------------- + +TEST(DgbExpectedTimeToBlock, ChainRealHashrateThenEtbMatchesOracle) { + // node.cpp path: raw aps -> real_pool_hs (1 - stale_prop scaling) -> etb. + const double pool_hs = 1.0e6; + const double stale_prop = 0.05; + const double real_hs = pool_hs / (1.0 - stale_prop); // pool_efficiency oracle + const double avg = 1.0e12; + const double oracle = avg / real_hs; // etb oracle + EXPECT_DOUBLE_EQ( + dgb::compute_expected_time_to_block(avg, real_hs, false, true), oracle); +} + +} // namespace