diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 992ab91ff..6a8d0ca4f 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 dgb_desired_version_tally_test dgb_get_height_and_last_endpoints_test dgb_naughty_propagation_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 dgb_get_height_and_last_endpoints_test dgb_chain_walk_window_test dgb_naughty_propagation_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 dgb_desired_version_tally_test dgb_get_height_and_last_endpoints_test dgb_naughty_propagation_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 dgb_get_height_and_last_endpoints_test dgb_chain_walk_window_test dgb_naughty_propagation_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/chain_walk_window.hpp b/src/impl/dgb/coin/chain_walk_window.hpp new file mode 100644 index 000000000..de3ce9759 --- /dev/null +++ b/src/impl/dgb/coin/chain_walk_window.hpp @@ -0,0 +1,75 @@ +#pragma once + +// SSOT for the DGB LOOKBEHIND chain-walk WINDOW clamp -- the pure integer guard +// every backward sharechain scan applies before calling forest.get_chain(). The +// same three-line pattern is open-coded in four share_tracker.hpp accessors: +// +// auto height = chain.get_height(share_hash); +// auto actual = std::min(static_cast(lookbehind), height); // clamp +// if (actual <= 0) return ; // guard +// auto view = chain.get_chain(share_hash, actual); // walk +// +// get_average_stale_prop (share_tracker.hpp:2050) +// get_stale_counts (share_tracker.hpp:2072) +// get_desired_version_counts (share_tracker.hpp:2114) +// get_desired_version_weights (share_tracker.hpp:2149) +// +// The clamp is the canonical p2pool windowing idiom: the caller asks for a +// `lookbehind` of N shares, but a head only `height` shares deep can yield at +// most `height` ancestors, so the realized window is min(lookbehind, height). +// A window of zero (genesis / shallower-than-1) must short-circuit to the empty +// result BEFORE the walk, exactly as the oracle does -- forest.get_chain over a +// zero count yields nothing and the per-share denominators (stale_count + actual) +// / weight maps degenerate. +// +// Oracle: p2pool-dgb-scrypt main.py status loop, which clamps every diagnostic +// lookbehind at the call site -- +// get_average_stale_prop(tracker, best, min(720, tracker.get_height(best))) +// get_desired_version_counts(tracker, best, min(self.tracker.get_height(best), ...)) +// -- and util/forest.py Tracker.get_chain(item_hash, n), which walks at most +// `n` parents and stops at the chain end. The clamp + (actual<=0) guard are the +// c2pool restatement of that min() and the implicit empty-walk. +// +// Two consensus-bearing consumers ride this window: +// * get_desired_version_weights feeds the check()-phase 60% work-weighted v36 +// switch gate (share_check step 2) -- a drifted window would re-scope the +// accept gate and break the p2pool-dgb-scrypt crossing-soak invariant. +// * get_average_stale_prop / get_stale_counts feed efficiency diagnostics +// (pool_efficiency.hpp) -- diagnostic, but pinned so a silent floor drift is +// caught at compile-test time. +// +// Per-coin isolation: dgb/ only. Header-only, additive, free functions over the +// already-resolved (height, lookbehind) pair -- the get_chain skip-list walk +// stays in the forest. This slice does NOT rewire share_tracker.hpp; the four +// call sites keep their inline std::min/(actual<=0) guards. That byte-identity +// delegation is the follow-on (mirrors #414 redistribute rewire). The lifted +// body is a verbatim copy of the inline clamp (same int32_t height, same +// std::min, same <=0 comparison), so the follow-on is provably value-identical. +// Consensus-neutral: pure arithmetic, no value semantics changed. + +#include +#include + +namespace dgb { + +// Realized lookbehind window: the number of ancestors a backward get_chain walk +// from a head `height` shares deep will yield when asked for `lookbehind`. +// p2pool/c2pool: actual = min(lookbehind, height) +// `lookbehind` is taken as int32_t here; the two uint64_t call sites +// (get_average_stale_prop / get_stale_counts) apply static_cast at the +// call exactly as today -- the cast stays at the call, so delegation is byte-id. +inline int32_t chain_walk_window_count(int32_t height, int32_t lookbehind) +{ + return std::min(lookbehind, height); +} + +// Walk activation: the four accessors short-circuit to the empty result when the +// realized window is non-positive (genesis / shallower-than-one). Returns true +// when the get_chain walk should actually run. +// c2pool: if (actual <= 0) return ; -> runs iff actual > 0 +inline bool chain_walk_window_active(int32_t actual) +{ + return actual > 0; +} + +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 07007d5ad..98a0ec45e 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -822,6 +822,20 @@ if (BUILD_TESTING AND GTest_FOUND) c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage dgb_coin pool sharechain) gtest_add_tests(dgb_auto_ratchet_tail_guard_test "" AUTO) + # dgb_chain_walk_window_test: FENCED, additive KAT pinning the LOOKBEHIND + # chain-walk WINDOW clamp in coin/chain_walk_window.hpp vs the + # p2pool-dgb-scrypt oracle idiom actual = min(lookbehind, height) with the + # (actual<=0) empty-walk guard, open-coded today in share_tracker.hpp + # get_average_stale_prop / get_stale_counts / get_desired_version_counts / + # get_desired_version_weights (forest.py get_chain under the main.py call-site + # clamp). Pure header (/) -> links only GTest. + # 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_chain_walk_window_test chain_walk_window_test.cpp) + target_link_libraries(dgb_chain_walk_window_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_chain_walk_window_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 diff --git a/src/impl/dgb/test/chain_walk_window_test.cpp b/src/impl/dgb/test/chain_walk_window_test.cpp new file mode 100644 index 000000000..2cb37cd4b --- /dev/null +++ b/src/impl/dgb/test/chain_walk_window_test.cpp @@ -0,0 +1,87 @@ +// dgb lookbehind chain-walk WINDOW clamp -- KAT. +// +// FENCED, additive (no production code touched this slice). Pins +// src/impl/dgb/coin/chain_walk_window.hpp against the p2pool-dgb-scrypt oracle +// idiom that governs every backward sharechain accessor: +// actual = min(lookbehind, height); if (actual <= 0) -> empty result +// open-coded today in share_tracker.hpp get_average_stale_prop / +// get_stale_counts / get_desired_version_counts / get_desired_version_weights, +// mirroring p2pool util/forest.py Tracker.get_chain (walk at most n parents, +// stop at chain end) under the main.py call-site min(lookbehind, height) clamp. +// +// Every expectation is hand-derived from the oracle min()/<=0 formula, NOT read +// from the code under test. The final case is non-circular: it re-implements the +// verbatim inline clamp+guard the four share_tracker accessors use today and +// asserts the SSOT is value-identical across a dense matrix -- so the +// byte-identity delegation follow-on is proven safe before it is written. +// share_tracker.hpp is NOT rewired here. MUST appear in BOTH this dir +// CMakeLists.txt AND the build.yml --target allowlist (#143 NOT_BUILT trap). + +#include + +#include +#include + +using namespace dgb; + +// --- realized window = min(lookbehind, height) ------------------------------ +TEST(DgbChainWalkWindow, ClampsToHeight) { + // lookbehind below height -> identity (the ask is satisfiable). + EXPECT_EQ(chain_walk_window_count(/*height=*/2880, /*lookbehind=*/720), 720); + EXPECT_EQ(chain_walk_window_count(100, 50), 50); + // lookbehind above height -> clamped to what the chain can yield. + EXPECT_EQ(chain_walk_window_count(/*height=*/30, /*lookbehind=*/720), 30); + EXPECT_EQ(chain_walk_window_count(0, 720), 0); + // exact boundary: equal -> that value. + EXPECT_EQ(chain_walk_window_count(720, 720), 720); + // one-deep chain. + EXPECT_EQ(chain_walk_window_count(1, 720), 1); +} + +// --- degenerate / defensive inputs ------------------------------------------ +TEST(DgbChainWalkWindow, NonPositiveInputs) { + // genesis head (height 0) -> zero window regardless of lookbehind. + EXPECT_EQ(chain_walk_window_count(0, 1), 0); + // zero lookbehind -> zero window (caller asked for nothing). + EXPECT_EQ(chain_walk_window_count(2880, 0), 0); + // negative height (should never occur, but min() must still pick it). + EXPECT_EQ(chain_walk_window_count(-5, 720), -5); +} + +// --- activation guard: actual > 0 ------------------------------------------- +TEST(DgbChainWalkWindow, ActivationGuard) { + // share_tracker accessors early-return the empty result when actual <= 0. + EXPECT_FALSE(chain_walk_window_active(0)); + EXPECT_FALSE(chain_walk_window_active(-3)); + EXPECT_TRUE(chain_walk_window_active(1)); + EXPECT_TRUE(chain_walk_window_active(2880)); +} + +// --- composite: the realized window is walked iff it is positive ------------- +TEST(DgbChainWalkWindow, ClampThenActivateComposite) { + // genesis: clamp to 0, guard fails -> no walk. + EXPECT_FALSE(chain_walk_window_active(chain_walk_window_count(0, 720))); + // one-deep: clamp to 1, guard passes -> walk one ancestor. + EXPECT_TRUE(chain_walk_window_active(chain_walk_window_count(1, 720))); + // deep chain, real lookbehind: clamp to 720, walk 720. + EXPECT_TRUE(chain_walk_window_active(chain_walk_window_count(5000, 720))); +} + +// --- NON-CIRCULAR: SSOT == verbatim inline clamp+guard over a dense matrix --- +// Re-implements the exact three-line pattern from the four share_tracker.hpp +// accessors WITHOUT calling the header, then proves the SSOT matches. This is +// the safety proof for the byte-identity delegation follow-on. +TEST(DgbChainWalkWindow, DelegationMatchesPreDelegationInline) { + for (int32_t height = -2; height <= 64; ++height) { + for (int32_t lookbehind = 0; lookbehind <= 64; ++lookbehind) { + // verbatim pre-delegation inline (see share_tracker.hpp:2149-2153): + int32_t inline_actual = std::min(lookbehind, height); + bool inline_runs = !(inline_actual <= 0); + + EXPECT_EQ(chain_walk_window_count(height, lookbehind), inline_actual) + << "height=" << height << " lookbehind=" << lookbehind; + EXPECT_EQ(chain_walk_window_active(inline_actual), inline_runs) + << "actual=" << inline_actual; + } + } +}