diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 317226563..324d1f500 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_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 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_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 test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/pool_attempts_per_second.hpp b/src/impl/dgb/coin/pool_attempts_per_second.hpp new file mode 100644 index 000000000..ec74fd824 --- /dev/null +++ b/src/impl/dgb/coin/pool_attempts_per_second.hpp @@ -0,0 +1,76 @@ +#pragma once + +// SSOT for the DGB POOL-ATTEMPTS-PER-SECOND estimator -- the pure arithmetic +// core of ShareTracker::get_pool_attempts_per_second(): given the resolved +// near/far endpoints of a sharechain window, compute the pool hashrate estimate +// +// attempts_per_second = attempts / max(1, near_ts - far_ts) +// +// where attempts is the cumulative-work delta over the window (work, or min_work +// when the min-work variant is requested) and the time span is clamped to a +// strictly-positive denominator. This figure feeds the operator-facing "Pool: +// " status line AND the PPLNS / expected-time-to-block windows, so a +// silent drift (flipped sign on the span, dropping the <=0 clamp, a non-integer +// divide) would misreport pool hashrate with NO compile error -- operator-facing +// behavior the V36 master-compat invariant pins to the p2pool reference. +// +// Oracle: p2pool-dgb-scrypt data.py get_pool_attempts_per_second(tracker, +// previous_share_hash, dist, min_work=False, integer=False): +// assert dist >= 2 +// near = tracker.items[previous_share_hash] +// far = tracker.items[tracker.get_nth_parent_hash(previous_share_hash, dist-1)] +// attempts = tracker.get_delta(near.hash, far.hash).work # or .min_work +// time = near.timestamp - far.timestamp +// if time <= 0: time = 1 +// return attempts//time # the integer=True path c2pool always takes +// +// The chain-walk halves of the oracle -- locating `far` via get_nth_parent_hash +// and summing the work delta via get_delta -- stay inside ShareTracker (they need +// the skip-list / TrackerView). This header captures ONLY the endpoint-resolution +// guards and the final clamp+divide as a free function over already-resolved +// inputs, so a KAT can pin the arithmetic with no NodeImpl / ShareTracker standup. +// +// Per-coin isolation: dgb/ only. Header-only, additive. This slice does NOT yet +// rewire share_tracker.hpp -- that is the byte-identity delegation follow-on. +// The lifted body is a verbatim copy of the inline arithmetic (same int32_t span +// type, same uint288 divide), so the follow-on is provably value-identical. +// Consensus-neutral: pure arithmetic, no value semantics changed. + +#include + +#include // uint288 + +namespace dgb { + +// Resolved endpoints of a get_pool_attempts_per_second window. The caller +// performs the chain walk (dist guard, near lookup, far via get_nth_parent_hash, +// work delta via get_delta) and hands the results here. +struct PoolAttemptsInputs { + int32_t dist; // requested window depth; oracle asserts dist >= 2 + bool near_in_chain; // previous_share_hash present in the tracker + bool far_resolved; // get_nth_parent_hash(dist-1) found AND in the tracker + uint288 attempts; // get_delta(near, far).work (or .min_work) -- caller picks + uint32_t near_ts; // near.timestamp + uint32_t far_ts; // far.timestamp +}; + +// attempts_per_second over the resolved window. +// +// Returns 0 when the window cannot be formed: dist < 2 (oracle assert), the near +// share is absent, or the far endpoint did not resolve. Otherwise the time span +// is near_ts - far_ts clamped up to 1 (matching the oracle `if time <= 0: time = +// 1`), and the result is the integer division attempts / span (the integer=True +// path c2pool always uses). uint288 throughout -- no overflow on the cumulative +// work numerator. +inline uint288 compute_pool_attempts_per_second(const PoolAttemptsInputs& in) { + if (in.dist < 2) return uint288(0); + if (!in.near_in_chain) return uint288(0); + if (!in.far_resolved) return uint288(0); + + int32_t time_span = static_cast(in.near_ts) - static_cast(in.far_ts); + if (time_span <= 0) time_span = 1; + + return in.attempts / uint288(time_span); +} + +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index eddce598c..cba3788c8 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -724,6 +724,18 @@ if (BUILD_TESTING AND GTest_FOUND) core c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage) gtest_add_tests(dgb_tail_score_endpoints_test "" AUTO) + # dgb_pool_attempts_per_second_test: FENCED, additive KAT pinning the pure + # arithmetic core of ShareTracker::get_pool_attempts_per_second lifted into + # coin/pool_attempts_per_second.hpp vs the p2pool data.py oracle + # (attempts // max(1, near_ts - far_ts), with the dist>=2 + endpoint guards). + # Links core for uint288. share_tracker.hpp 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_pool_attempts_per_second_test pool_attempts_per_second_test.cpp) + target_link_libraries(dgb_pool_attempts_per_second_test PRIVATE + 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_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/pool_attempts_per_second_test.cpp b/src/impl/dgb/test/pool_attempts_per_second_test.cpp new file mode 100644 index 000000000..c911d37ca --- /dev/null +++ b/src/impl/dgb/test/pool_attempts_per_second_test.cpp @@ -0,0 +1,107 @@ +// dgb::compute_pool_attempts_per_second -- pool attempts/second KAT. +// +// FENCED conformance test (no production code touched). Pins the pure arithmetic +// core of ShareTracker::get_pool_attempts_per_second, lifted into +// pool_attempts_per_second.hpp, against the p2pool-dgb-scrypt oracle data.py +// get_pool_attempts_per_second: +// assert dist >= 2 +// attempts = get_delta(near, far).work # or .min_work +// time = near.timestamp - far.timestamp +// if time <= 0: time = 1 +// return attempts//time +// +// All expected values are HAND-DERIVED from that oracle expression (not produced +// by calling the helper under test), so the test is non-circular: it +// independently recomputes the oracle result 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 { + +using dgb::PoolAttemptsInputs; +using dgb::compute_pool_attempts_per_second; + +// Convenience: a fully-resolved window (both endpoints present). +PoolAttemptsInputs window(int32_t dist, const uint288& attempts, + uint32_t near_ts, uint32_t far_ts) { + return PoolAttemptsInputs{dist, /*near_in_chain=*/true, /*far_resolved=*/true, + attempts, near_ts, far_ts}; +} + +// ---- dist guard (oracle: assert dist >= 2) ------------------------------ + +TEST(DgbPoolAttempts, DistBelowTwoIsZero) { + // dist 1 / 0 / negative -> no window -> 0, even with attempts set. + EXPECT_EQ(compute_pool_attempts_per_second(window(1, uint288(1000), 100, 90)), uint288(0)); + EXPECT_EQ(compute_pool_attempts_per_second(window(0, uint288(1000), 100, 90)), uint288(0)); + EXPECT_EQ(compute_pool_attempts_per_second(window(-5, uint288(1000), 100, 90)), uint288(0)); +} + +TEST(DgbPoolAttempts, DistExactlyTwoIsAllowed) { + // dist == 2 is the minimum valid window; 1000 attempts over 10s -> 100. + EXPECT_EQ(compute_pool_attempts_per_second(window(2, uint288(1000), 100, 90)), uint288(100)); +} + +// ---- endpoint-resolution guards ----------------------------------------- + +TEST(DgbPoolAttempts, NearNotInChainIsZero) { + PoolAttemptsInputs in{2, /*near_in_chain=*/false, /*far_resolved=*/true, + uint288(1000), 100, 90}; + EXPECT_EQ(compute_pool_attempts_per_second(in), uint288(0)); +} + +TEST(DgbPoolAttempts, FarUnresolvedIsZero) { + PoolAttemptsInputs in{2, /*near_in_chain=*/true, /*far_resolved=*/false, + uint288(1000), 100, 90}; + EXPECT_EQ(compute_pool_attempts_per_second(in), uint288(0)); +} + +// ---- core divide: attempts / span --------------------------------------- + +TEST(DgbPoolAttempts, AttemptsOverSpan) { + // 6000 work over (200-150)=50s -> 120 (hand-derived). + EXPECT_EQ(compute_pool_attempts_per_second(window(5, uint288(6000), 200, 150)), uint288(120)); +} + +TEST(DgbPoolAttempts, IntegerTruncation) { + // Oracle uses integer division: 1000 // 7 = 142 (142.857... truncated). + EXPECT_EQ(compute_pool_attempts_per_second(window(3, uint288(1000), 107, 100)), uint288(142)); +} + +// ---- time-span clamp (oracle: if time <= 0: time = 1) ------------------- + +TEST(DgbPoolAttempts, ZeroSpanClampsToOne) { + // near_ts == far_ts -> span 0 -> clamp to 1 -> attempts/1 = attempts. + EXPECT_EQ(compute_pool_attempts_per_second(window(2, uint288(777), 500, 500)), uint288(777)); +} + +TEST(DgbPoolAttempts, NegativeSpanClampsToOne) { + // far newer than near (span -10) -> clamp to 1 -> attempts unchanged. + EXPECT_EQ(compute_pool_attempts_per_second(window(2, uint288(555), 90, 100)), uint288(555)); +} + +// ---- attempts is used verbatim (work vs min_work both flow through) ----- + +TEST(DgbPoolAttempts, AttemptsValuePassedThroughRegardlessOfSource) { + // The function is agnostic to whether attempts came from delta.work or + // delta.min_work -- the caller selects; pin that the chosen value divides. + EXPECT_EQ(compute_pool_attempts_per_second(window(4, uint288(2000), 220, 200)), uint288(100)); // work-like + EXPECT_EQ(compute_pool_attempts_per_second(window(4, uint288(400), 220, 200)), uint288(20)); // min_work-like +} + +// ---- wide numerator: no overflow truncation at 64 bits ------------------ + +TEST(DgbPoolAttempts, WideAttemptsNoOverflow) { + // attempts = 2^200, span 2 -> 2^199. Far beyond uint64; uint288 holds it. + uint288 big = uint288(1); big <<= 200; + uint288 half = uint288(1); half <<= 199; + EXPECT_EQ(compute_pool_attempts_per_second(window(2, big, 12, 10)), half); +} + +} // namespace