diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52539f69e..86fc65df0 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_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_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_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_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/tail_score_endpoints.hpp b/src/impl/dgb/coin/tail_score_endpoints.hpp new file mode 100644 index 000000000..88b72d54f --- /dev/null +++ b/src/impl/dgb/coin/tail_score_endpoints.hpp @@ -0,0 +1,121 @@ +#pragma once + +// SSOT for the DGB TAIL-HASHRATE-SCORE chain-walk endpoints -- the pure +// arithmetic core of ShareTracker::score() (the per-tail "approximate lower +// bound on the chain's hashrate" used to rank candidate sharechain tails). +// Given a verified head share it resolves a window +// +// end_point = get_nth_parent_hash(share_hash, CHAIN_LENGTH * 15 / 16) +// +// walks the trailing CHAIN_LENGTH/16 shares to find the most-recent resolvable +// parent block, then scores +// +// score = get_delta(share_hash, end_point).work / (block_span * BLOCK_PERIOD) +// +// This score decides best_tail selection (data.py:767-772) and therefore which +// sharechain the pool extends -- a silent drift in the 15/16 endpoint offset, the +// /16 tail length, the short-chain guard, or the span clamp would re-rank tails +// with NO compile error, diverging operator-facing chain selection from the +// p2pool reference the V36 master-compat invariant pins. +// +// Oracle: p2pool-dgb-scrypt data.py:843-855 ShareTracker.score(share_hash, +// block_rel_height_func): +// head_height = self.verified.get_height(share_hash) +// if head_height < self.net.CHAIN_LENGTH: +// return head_height, None +// end_point = self.verified.get_nth_parent_hash( +// share_hash, self.net.CHAIN_LENGTH*15//16) +// block_height = max(block_rel_height_func(share.header['previous_block']) +// for share in self.verified.get_chain(end_point, self.net.CHAIN_LENGTH//16)) +// return self.net.CHAIN_LENGTH, \ +// self.verified.get_delta(share_hash, end_point).work \ +// / ((0 - block_height + 1) * self.net.PARENT.BLOCK_PERIOD) +// +// The chain-walk halves -- locating end_point via get_nth_parent_hash, walking +// get_chain, and summing the work delta via get_delta -- stay inside ShareTracker +// (they need the skip-list / TrackerView). This header captures ONLY the integer +// endpoint offsets, the short-chain guard, the unresolvable-block fallback, and +// the final span-clamp+divide as free functions over already-resolved inputs, so +// a KAT can pin the arithmetic with no NodeImpl / ShareTracker standup. +// +// Confirmation convention: p2pool's block_rel_height_func returns a relative +// height (0 = tip, negative = behind) and scores over (0 - block_height + 1) * +// BLOCK_PERIOD. c2pool's block_rel_height_func returns a CONFIRMATION count +// (1 = tip, 4 = three-deep), so the equivalent span is block_height * +// BLOCK_PERIOD: oracle (0 - 0 + 1) = 1 == c2pool 1 (tip), oracle (0 - -3 + 1) = 4 +// == c2pool 4. The mapping is exact; this SSOT works in the c2pool confirmation +// convention to match the inline body verbatim. +// +// 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 bodies are verbatim copies 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 + +#include // uint288 + +namespace dgb { + +// Distance to the window end-point: get_nth_parent_hash(share_hash, this). +// Oracle: self.net.CHAIN_LENGTH*15//16 (Python floor division == C++ / on +// non-negative operands). CHAIN_LENGTH is a positive net constant. +inline int32_t score_endpoint_offset(int32_t chain_length) +{ + return (chain_length * 15) / 16; +} + +// Number of trailing shares walked from end_point to find the most-recent +// resolvable parent block. Oracle: get_chain(end_point, self.net.CHAIN_LENGTH//16). +// The inline body additionally clamps to the end_point's accumulated height so +// the walk never over-reads past chain end (get_chain stops there anyway, but the +// clamp makes the <= 0 short-circuit below exact). end_point_acc_height is +// verified.get_acc_height(end_point). +inline int32_t score_tail_walk_count(int32_t chain_length, int32_t end_point_acc_height) +{ + return std::min(chain_length / 16, end_point_acc_height); +} + +// Short-chain guard: when the head's accumulated verified height is below a full +// CHAIN_LENGTH the score is undefined (oracle returns (head_height, None)). The +// caller returns {head_acc_height, 0} in that case. +inline bool score_head_too_short(int32_t head_acc_height, int32_t chain_length) +{ + return head_acc_height < chain_length; +} + +// Resolved block span in the c2pool confirmation convention. When NO parent block +// in the trailing window resolves (block_rel_height_func returned <= 0 for all, or +// the window was empty), p2pool would score work / (1e9 * BLOCK_PERIOD) -- tiny but +// non-zero so the higher-work chain still wins and selection does not oscillate. +// c2pool matches with a very large confirmation count (1,000,000) producing the +// same tiny-but-positive score. raw_block_height is the max confirmation count +// found; resolved is false when no block resolved. +inline int32_t score_resolved_block_span(bool resolved, int32_t raw_block_height) +{ + if (!resolved || raw_block_height <= 0) + return 1000000; + return raw_block_height; +} + +// Score denominator: block_span * BLOCK_PERIOD, clamped strictly positive so the +// divide is well-defined. Oracle BLOCK_PERIOD = self.net.PARENT.BLOCK_PERIOD +// (config_coin.hpp = 75s). block_period is the make_coin_params-populated value. +inline int32_t score_time_span(int32_t block_span, int32_t block_period) +{ + int32_t time_span = block_span * block_period; + if (time_span <= 0) + time_span = 1; + return time_span; +} + +// Final hashrate score: cumulative-work delta over the window divided by the +// clamped time span. total_work = verified.get_delta_work(share_hash, end_point). +inline uint288 score_value(const uint288& total_work, int32_t time_span) +{ + return total_work / static_cast(time_span); +} + +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 6058a20ba..eddce598c 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -708,6 +708,22 @@ if (BUILD_TESTING AND GTest_FOUND) 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_tail_score_endpoints_test: FENCED, additive KAT pinning the + # TAIL-HASHRATE-SCORE chain-walk endpoint arithmetic in + # coin/tail_score_endpoints.hpp vs the p2pool-dgb-scrypt data.py:843-855 + # ShareTracker.score() oracle (end-point offset CHAIN_LENGTH*15//16, trailing + # walk CHAIN_LENGTH//16, short-chain guard, unresolvable-block 1e6 fallback, + # work/(block_span*BLOCK_PERIOD) divide). Pure header (uint288) -> links core. + # 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_tail_score_endpoints_test tail_score_endpoints_test.cpp) + target_link_libraries(dgb_tail_score_endpoints_test PRIVATE + GTest::gtest_main GTest::gtest + core + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage) + gtest_add_tests(dgb_tail_score_endpoints_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/tail_score_endpoints_test.cpp b/src/impl/dgb/test/tail_score_endpoints_test.cpp new file mode 100644 index 000000000..8fa9d18e5 --- /dev/null +++ b/src/impl/dgb/test/tail_score_endpoints_test.cpp @@ -0,0 +1,106 @@ +// dgb tail-hashrate-score endpoints — chain-walk endpoint arithmetic KAT. +// +// FENCED, additive (no production code touched this slice). Pins +// src/impl/dgb/coin/tail_score_endpoints.hpp against the p2pool-dgb-scrypt +// data.py:843-855 ShareTracker.score() oracle: +// head_height = self.verified.get_height(share_hash) +// if head_height < self.net.CHAIN_LENGTH: return head_height, None +// end_point = self.verified.get_nth_parent_hash(share_hash, CHAIN_LENGTH*15//16) +// block_height = max(block_rel_height_func(...) for share in +// self.verified.get_chain(end_point, CHAIN_LENGTH//16)) +// return CHAIN_LENGTH, get_delta(share_hash, end_point).work +// / ((0 - block_height + 1) * PARENT.BLOCK_PERIOD) +// +// Every expectation is hand-derived from the oracle formula and the DGB net +// constants (CHAIN_LENGTH = 2880 mainnet / 400 testnet, PARENT.BLOCK_PERIOD = +// 75s), NOT read from the code under test. The chain-walk halves (get_nth_parent_hash, +// get_chain, get_delta) stay in ShareTracker; this header lifts only the integer +// endpoint offsets, the short-chain guard, the unresolvable-block fallback, and +// the span-clamp+divide. 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 + +// --- end-point offset = CHAIN_LENGTH*15//16 (Python floor div) -------------- +TEST(DgbTailScoreEndpoints, EndpointOffsetMatchesOracle) { + EXPECT_EQ(dgb::score_endpoint_offset(2880), 2700); // 2880*15/16 = 43200/16 + EXPECT_EQ(dgb::score_endpoint_offset(400), 375); // testnet: 400*15/16 = 6000/16 + EXPECT_EQ(dgb::score_endpoint_offset(16), 15); // 16*15/16 = 15 + EXPECT_EQ(dgb::score_endpoint_offset(32), 30); // 480/16 + EXPECT_EQ(dgb::score_endpoint_offset(0), 0); +} + +// --- trailing-walk length = min(CHAIN_LENGTH//16, end_point_acc_height) ----- +TEST(DgbTailScoreEndpoints, TailWalkCountMatchesOracleAndClamps) { + EXPECT_EQ(dgb::score_tail_walk_count(2880, 5000), 180); // min(180, 5000) + EXPECT_EQ(dgb::score_tail_walk_count(2880, 100), 100); // clamp to acc-height + EXPECT_EQ(dgb::score_tail_walk_count(2880, 180), 180); // exact boundary + EXPECT_EQ(dgb::score_tail_walk_count(400, 5000), 25); // testnet 400/16 + EXPECT_EQ(dgb::score_tail_walk_count(2880, 0), 0); // <=0 -> caller early-returns +} + +// --- short-chain guard: head_height < CHAIN_LENGTH -> score undefined ------- +TEST(DgbTailScoreEndpoints, ShortChainGuardMatchesOracle) { + EXPECT_TRUE (dgb::score_head_too_short(2879, 2880)); // one short of a full chain + EXPECT_FALSE(dgb::score_head_too_short(2880, 2880)); // exactly full -> scored + EXPECT_FALSE(dgb::score_head_too_short(5000, 2880)); // long -> scored + EXPECT_TRUE (dgb::score_head_too_short(399, 400)); // testnet short + EXPECT_FALSE(dgb::score_head_too_short(400, 400)); // testnet full +} + +// --- unresolvable-block fallback: 1e6 confirmations (tiny but non-zero) ----- +TEST(DgbTailScoreEndpoints, UnresolvableBlockFallbackMatchesOracle) { + EXPECT_EQ(dgb::score_resolved_block_span(true, 4), 4); // resolved, 3-deep + EXPECT_EQ(dgb::score_resolved_block_span(true, 1), 1); // resolved, tip + EXPECT_EQ(dgb::score_resolved_block_span(false, 4), 1000000); // window had none + EXPECT_EQ(dgb::score_resolved_block_span(true, 0), 1000000); // resolved <= 0 + EXPECT_EQ(dgb::score_resolved_block_span(true, -3), 1000000); // negative -> fallback +} + +// --- time span = block_span * BLOCK_PERIOD, clamped strictly positive ------- +TEST(DgbTailScoreEndpoints, TimeSpanMatchesOracleAndClamps) { + EXPECT_EQ(dgb::score_time_span(1, 75), 75); // tip: 1*75 + EXPECT_EQ(dgb::score_time_span(4, 75), 300); // 4-deep: 4*75 + EXPECT_EQ(dgb::score_time_span(1000000, 75), 75000000); // fallback span + EXPECT_EQ(dgb::score_time_span(0, 75), 1); // clamp <=0 + EXPECT_EQ(dgb::score_time_span(1, 0), 1); // zero period -> clamp +} + +// --- final score = work / time_span (integer divide) ----------------------- +TEST(DgbTailScoreEndpoints, ScoreValueMatchesOracle) { + EXPECT_EQ(dgb::score_value(uint288(300), 75), uint288(4)); // 300/75 + EXPECT_EQ(dgb::score_value(uint288(900), 300), uint288(3)); // 900/300 + EXPECT_EQ(dgb::score_value(uint288(0), 75), uint288(0)); // no work + EXPECT_EQ(dgb::score_value(uint288(1000000000ULL), 75000000), uint288(13)); // 1e9/75e6 +} + +// --- non-circular composite: full oracle scoring, hand-derived literals ----- +// Case A (resolved, 4-deep tip block): CHAIN_LENGTH=2880, BLOCK_PERIOD=75, +// work=900000, block_height=4 (resolved). +// not short (2880>=2880); span=4; time_span=4*75=300; score=900000/300=3000. +TEST(DgbTailScoreEndpoints, CompositeResolvedScoreHandDerived) { + ASSERT_FALSE(dgb::score_head_too_short(2880, 2880)); + int32_t span = dgb::score_resolved_block_span(true, 4); // 4 + int32_t time_span = dgb::score_time_span(span, 75); // 300 + uint288 score = dgb::score_value(uint288(900000), time_span); + EXPECT_EQ(span, 4); + EXPECT_EQ(time_span, 300); + EXPECT_EQ(score, uint288(3000)); // 900000 / 300, hand-computed +} + +// Case B (unresolvable window): work=750000000, no block resolves. +// span=1000000; time_span=1000000*75=75000000; score=750000000/75000000=10. +TEST(DgbTailScoreEndpoints, CompositeUnresolvableScoreHandDerived) { + int32_t span = dgb::score_resolved_block_span(false, 0); // 1000000 + int32_t time_span = dgb::score_time_span(span, 75); // 75000000 + uint288 score = dgb::score_value(uint288(750000000ULL), time_span); + EXPECT_EQ(span, 1000000); + EXPECT_EQ(time_span, 75000000); + EXPECT_EQ(score, uint288(10)); // tiny but non-zero, hand-computed +}