diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2c9276ae..eb7444ebb 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 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 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 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 test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/desired_version_tally.hpp b/src/impl/dgb/coin/desired_version_tally.hpp new file mode 100644 index 000000000..6879931ff --- /dev/null +++ b/src/impl/dgb/coin/desired_version_tally.hpp @@ -0,0 +1,83 @@ +#pragma once + +// SSOT for the DGB DESIRED-VERSION TALLY -- the pure accumulation core of +// ShareTracker::get_desired_version_weights() (and its flat-count diagnostic +// sibling get_desired_version_counts()). Walking back over a clamped +// [.., CHAIN_LENGTH] window of shares, each share's m_desired_version is +// bucketed and either its work is summed (the consensus map) or its occurrence +// counted (the diagnostic map). The work-weighted map is the CONSENSUS input to +// the V36 60%-by-work switch rule (share_check step 2) and the #288 AutoRatchet +// activation tail guard; the flat-count map is diagnostics/KAT only and is +// NEVER the gate. +// +// Oracle: p2pool-dgb-scrypt data.py:918-922 get_desired_version_counts: +// def get_desired_version_counts(tracker, best_share_hash, dist): +// res = {} +// for share in tracker.get_chain(best_share_hash, dist): +// res[share.desired_version] = res.get(share.desired_version, 0) \ +// + bitcoin_data.target_to_average_attempts(share.target) +// return res +// +// NOTE the oracle's get_desired_version_counts is ALREADY work-weighted -- each +// share contributes target_to_average_attempts(share.target), NOT 1. c2pool +// splits this into two accessors: get_desired_version_weights (the true oracle +// match -- weight = ShareIndex::work = chain::target_to_average_attempts( +// chain::bits_to_target(m_bits)), the CONSENSUS gate input) and +// get_desired_version_counts (a flat occurrence count, diagnostics-only, never +// the gate -- see the share_tracker.hpp comment, #406 and #288). This header +// captures ONLY the per-version accumulation over already-resolved +// (desired_version, work) pairs as free functions; the chain-walk and the +// lookbehind clamp stay inside ShareTracker (the clamp is the separate +// chain_walk_window SSOT). share_tracker.hpp is NOT rewired this slice +// (byte-identity delegation is the follow-on). +// +// Per-coin isolation: dgb/ only. Header-only, additive, consensus-neutral (pure +// std::map accumulation, no value semantics changed). MUST appear in BOTH this +// dir CMakeLists.txt AND the build.yml --target allowlist, or it becomes a #143 +// NOT_BUILT sentinel. + +#include // uint288 + +#include +#include +#include + +namespace dgb { + +// One share's contribution to the tally: its desired_version and its work +// (= chain::target_to_average_attempts(chain::bits_to_target(m_bits)), the same +// value ShareIndex::work caches). Mirrors the (dv, idx->work) pair the inline +// loop reads per share in get_desired_version_weights. +struct VersionWork { + uint64_t desired_version; + uint288 work; +}; + +// Work-weighted version tally -- the CONSENSUS gate input. Mirrors verbatim the +// inline body: +// for (share in window) weights[dv] = weights[dv] + idx->work; +// std::map value-initializes uint288(0) on first touch of a version key, exactly +// matching the inline weights[dv] + idx->work (and the oracle +// res.get(dv, 0) + attempts). +inline std::map +accumulate_version_weights(const std::vector& window) +{ + std::map weights; + for (const auto& s : window) + weights[s.desired_version] = weights[s.desired_version] + s.work; + return weights; +} + +// Flat occurrence count -- DIAGNOSTICS/KAT ONLY, never the consensus gate. Each +// share counts as 1 regardless of its work. Mirrors verbatim the inline body: +// for (share in window) counts[dv]++; +inline std::map +accumulate_version_counts(const std::vector& window) +{ + std::map counts; + for (uint64_t dv : window) + counts[dv]++; + return counts; +} + +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 6e3033c3c..e0035a9d5 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -748,6 +748,24 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(dgb_binomial_conf_interval_test PRIVATE GTest::gtest_main GTest::gtest) gtest_add_tests(dgb_binomial_conf_interval_test "" AUTO) + # dgb_desired_version_tally_test: FENCED, additive KAT pinning the + # version->work / version->count accumulation core lifted into + # coin/desired_version_tally.hpp vs the p2pool-dgb-scrypt data.py:918-922 + # get_desired_version_counts oracle (res[dv] += target_to_average_attempts( + # share.target)). The work-weighted map is the CONSENSUS 60%-by-work switch + # input (share_check step 2 / #288 AutoRatchet tail guard); the flat-count + # accumulator is diagnostics-only and pinned just to lock the documented + # divergence. The work-equivalence anchor uses chain::target_to_average_attempts + # over 2^k-1 targets (hand-derived ttaa), so links core for uint288 + + # target_utils. 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_desired_version_tally_test desired_version_tally_test.cpp) + target_link_libraries(dgb_desired_version_tally_test PRIVATE + GTest::gtest_main GTest::gtest + core + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage) + gtest_add_tests(dgb_desired_version_tally_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/desired_version_tally_test.cpp b/src/impl/dgb/test/desired_version_tally_test.cpp new file mode 100644 index 000000000..10e3254a2 --- /dev/null +++ b/src/impl/dgb/test/desired_version_tally_test.cpp @@ -0,0 +1,116 @@ +// dgb desired-version tally -- version->work / version->count accumulation KAT. +// +// FENCED, additive (no production code touched this slice). Pins +// src/impl/dgb/coin/desired_version_tally.hpp against the p2pool-dgb-scrypt +// data.py:918-922 get_desired_version_counts oracle: +// res = {} +// for share in tracker.get_chain(best_share_hash, dist): +// res[share.desired_version] = res.get(share.desired_version, 0) \ +// + bitcoin_data.target_to_average_attempts(share.target) +// return res +// +// The oracle weights each share by target_to_average_attempts(share.target); +// c2pool exposes that as get_desired_version_weights (CONSENSUS gate input) with +// per-share weight = ShareIndex::work = chain::target_to_average_attempts( +// chain::bits_to_target(m_bits)). The flat-count accumulator is diagnostics-only +// and has NO oracle equivalent (the oracle has no flat count) -- it is pinned +// here only to lock the documented divergence (each share == 1, work ignored). +// +// Every expectation is hand-derived from the oracle formula. The work-equivalence +// anchor uses targets whose target_to_average_attempts is exactly computable by +// hand: ttaa(t) = 2^256 / (t + 1), so for t = 2^k - 1, t + 1 = 2^k and +// ttaa = 2^(256-k). The chain-walk + lookbehind clamp stay in ShareTracker; this +// header lifts ONLY the per-version map accumulation. share_tracker.hpp is NOT +// rewired (delegation is the byte-identity follow-on). Pure header (uint288) -> +// links core only. MUST appear in BOTH this dir CMakeLists.txt AND the build.yml +// --target allowlist, or it becomes a #143 NOT_BUILT sentinel. + +#include + +#include // uint288 +#include // chain::target_to_average_attempts + +#include + +namespace { + +uint288 u(uint64_t n) { return uint288(n); } + +} // namespace + +// --- empty window -> empty maps (the actual<=0 early-return shape) ---------- +TEST(DgbDesiredVersionTally, EmptyWindowYieldsEmptyMaps) { + EXPECT_TRUE(dgb::accumulate_version_weights({}).empty()); + EXPECT_TRUE(dgb::accumulate_version_counts({}).empty()); +} + +// --- single share: one bucket, weight == its work, count == 1 --------------- +TEST(DgbDesiredVersionTally, SingleShareSingleBucket) { + auto w = dgb::accumulate_version_weights({{36, u(42)}}); + ASSERT_EQ(w.size(), 1u); + EXPECT_EQ(w[36], u(42)); + + auto c = dgb::accumulate_version_counts({36}); + ASSERT_EQ(c.size(), 1u); + EXPECT_EQ(c[36], 1); +} + +// --- same version, many shares: weights sum, count increments --------------- +TEST(DgbDesiredVersionTally, SameVersionAccumulates) { + auto w = dgb::accumulate_version_weights({{36, u(10)}, {36, u(20)}, {36, u(30)}}); + ASSERT_EQ(w.size(), 1u); + EXPECT_EQ(w[36], u(60)); // 10 + 20 + 30 + + auto c = dgb::accumulate_version_counts({36, 36, 36}); + EXPECT_EQ(c[36], 3); +} + +// --- multiple versions are bucketed independently --------------------------- +TEST(DgbDesiredVersionTally, MultipleVersionsBucketed) { + auto w = dgb::accumulate_version_weights( + {{35, u(5)}, {36, u(11)}, {35, u(7)}}); + ASSERT_EQ(w.size(), 2u); + EXPECT_EQ(w[35], u(12)); // 5 + 7 + EXPECT_EQ(w[36], u(11)); + + auto c = dgb::accumulate_version_counts({35, 36, 35}); + EXPECT_EQ(c[35], 2); + EXPECT_EQ(c[36], 1); +} + +// --- NON-CIRCULAR oracle anchor: per-share weight == target_to_average_attempts, +// summed per version, matches data.py:918 res[dv] += ttaa(target). --------- +// t = 2^255-1 -> ttaa = 2 ; t = 2^254-1 -> ttaa = 4 ; t = 2^253-1 -> ttaa = 8 +TEST(DgbDesiredVersionTally, WorkWeightedMatchesOracleAttempts) { + uint256 t255, t254, t253; + t255.SetHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // 2^255-1 + t254.SetHex("3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // 2^254-1 + t253.SetHex("1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // 2^253-1 + + const uint288 a255 = chain::target_to_average_attempts(t255); + const uint288 a254 = chain::target_to_average_attempts(t254); + const uint288 a253 = chain::target_to_average_attempts(t253); + + // Hand-derived ttaa = 2^(256-k). These also non-circularly pin the work def. + ASSERT_EQ(a255, u(2)); + ASSERT_EQ(a254, u(4)); + ASSERT_EQ(a253, u(8)); + + // v36 has two shares (work 2 and 4), v35 has one share (work 8). + auto w = dgb::accumulate_version_weights( + {{36, a255}, {35, a253}, {36, a254}}); + ASSERT_EQ(w.size(), 2u); + EXPECT_EQ(w[36], u(6)); // 2 + 4 + EXPECT_EQ(w[35], u(8)); // 8 +} + +// --- flat count is NOT work-weighted: pins the documented divergence --------- +TEST(DgbDesiredVersionTally, FlatCountIgnoresWork) { + // Three v36 shares with wildly different work: weights sum the work, + // counts treat each as exactly 1 (diagnostics-only, never the gate). + auto w = dgb::accumulate_version_weights({{36, u(1)}, {36, u(1000)}, {36, u(7)}}); + EXPECT_EQ(w[36], u(1008)); + + auto c = dgb::accumulate_version_counts({36, 36, 36}); + EXPECT_EQ(c[36], 3); // not 1008 -- work is ignored +}