diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 11527ce40..992ab91ff 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_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_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_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_naughty_propagation_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/get_height_and_last_endpoints.hpp b/src/impl/dgb/coin/get_height_and_last_endpoints.hpp new file mode 100644 index 000000000..4cde24cda --- /dev/null +++ b/src/impl/dgb/coin/get_height_and_last_endpoints.hpp @@ -0,0 +1,113 @@ +#pragma once + +// SSOT for the DGB GET_HEIGHT_AND_LAST chain-walk endpoints -- the pure integer +// arithmetic that every consumer of forest.get_height_and_last() applies to the +// resolved (height, last) pair before walking the sharechain. forest returns +// +// (delta.height, delta.tail) [p2pool util/forest.py:171-173] +// +// where `last` (delta.tail) is the hash one past the deepest reachable ancestor, +// or null when the walk bottoms out at a genesis/rooted tail. Two facts about +// that pair drive consensus-bearing windowing across the DGB tree: +// +// 1. The ROOTED-TAIL invariant. A verified head is only fully rooted once it +// has >= REAL_CHAIN_LENGTH ancestors; below that it must still be hanging +// off an unrooted tail (last is None). p2pool asserts this at every window +// entry point: +// data.py:161 assert height >= net.REAL_CHAIN_LENGTH or last is None +// data.py:696 if height < CHAIN_LENGTH + 1 and last is not None: raise +// +// 2. The PPLNS / payout WINDOW DEPTH. The trailing payout walk is clamped to +// the configured chain length and is only live once at least one share is +// in range: +// depth = min(height, REAL_CHAIN_LENGTH) (redistribute.hpp:453) +// active = depth >= 1 +// which mirrors the oracle PPLNS bound +// _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) +// (see pplns_weight_walk.hpp:96 / p2pool get_chain(best, min(height, CL))). +// +// 3. The MONITOR min-height gate. The pool monitor's diagnostic cycle is a +// no-op until the chain is deep enough to be statistically meaningful: +// if (height < 10) return 0; (pool_monitor.hpp:98) +// Diagnostic-only, but pinned here so a silent drift of the floor is caught. +// +// A silent drift in the REAL_CHAIN_LENGTH clamp, the >=1 activation, or the +// rooted-tail invariant would re-window PPLNS payout with NO compile error -- +// diverging operator-facing reward distribution from the p2pool-dgb-scrypt +// reference the V36 master-compat invariant pins. +// +// Oracle: p2pool-dgb-scrypt util/forest.py:171-173 (get_height_and_last) + +// data.py:160-161 (generate_transaction rooted-tail assert) + +// data.py:695-696 (attempt_verify rooted-tail raise) + +// networks/digibyte.py (REAL_CHAIN_LENGTH = 12*60*60//15 = 2880). +// +// Per-coin isolation: dgb/ only. Header-only, additive, free functions over the +// already-resolved (height, last) pair -- the get_delta_to_last skip-list walk +// stays in the forest. This slice does NOT rewire redistribute.hpp / pool_monitor.hpp +// / share_check.hpp -- that is the byte-identity delegation follow-on. The lifted +// bodies are verbatim copies of the inline guards (same int32_t height type, +// same min()/comparison), so the follow-on is provably value-identical. +// Consensus-neutral: pure arithmetic, no value semantics changed. + +#include +#include + +namespace dgb { + +// Minimum sharechain depth before the pool monitor's diagnostic cycle runs. +// p2pool/c2pool pool_monitor.hpp:98 -- diagnostic floor, not consensus. +static constexpr int32_t MONITOR_MIN_HEIGHT = 10; + +// ROOTED-TAIL invariant (data.py:161, data.py:696). A resolved head must either +// have at least `real_chain_length` ancestors, or still be hanging off an +// unrooted tail (last is null). Returns true when the invariant holds. +// p2pool: height >= net.REAL_CHAIN_LENGTH or last is None +inline bool rooted_tail_invariant_holds(int32_t height, + bool last_is_null, + int32_t real_chain_length) +{ + return height >= real_chain_length || last_is_null; +} + +// attempt_verify entry guard (data.py:695-696): a share below CHAIN_LENGTH+1 +// height that is NOT on an unrooted tail is malformed and must be rejected. +// Returns true when the share is acceptable for verification at this depth. +// p2pool: if height < CHAIN_LENGTH + 1 and last is not None: raise +inline bool verify_depth_ok(int32_t height, bool last_is_null, int32_t chain_length) +{ + return height >= chain_length + 1 || last_is_null; +} + +// PPLNS / payout window depth (redistribute.hpp:453). Trailing payout walk is +// clamped to the configured real chain length. +// c2pool: depth = min(height, real_chain_length()) +inline int32_t pplns_window_depth(int32_t height, int32_t real_chain_length) +{ + return std::min(height, real_chain_length); +} + +// PPLNS window activation (redistribute.hpp:454). The payout window is only live +// once at least one share is in range. +// c2pool: if (depth < 1) return; -> active iff depth >= 1 +inline bool pplns_window_active(int32_t depth) +{ + return depth >= 1; +} + +// p2pool PPLNS share-count bound (pplns_weight_walk.hpp:96): the number of +// shares actually contributing to PPLNS weight, one less than the window depth +// (the head itself is excluded), floored at zero. +// p2pool: _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) +inline int32_t pplns_max_shares(int32_t height, int32_t real_chain_length) +{ + return std::max(0, pplns_window_depth(height, real_chain_length) - 1); +} + +// Pool-monitor diagnostic gate (pool_monitor.hpp:98). +// c2pool: if (height < 10) return 0; -> runs iff height >= 10 +inline bool monitor_cycle_runs(int32_t height) +{ + return height >= MONITOR_MIN_HEIGHT; +} + +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 7cf43ce23..07007d5ad 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -780,6 +780,21 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(dgb_naughty_propagation_test PRIVATE GTest::gtest_main GTest::gtest) gtest_add_tests(dgb_naughty_propagation_test "" AUTO) + # dgb_get_height_and_last_endpoints_test: FENCED, additive KAT pinning the + # get_height_and_last chain-walk WINDOW arithmetic in + # coin/get_height_and_last_endpoints.hpp vs the p2pool-dgb-scrypt oracle: + # forest.py:171-173 (height,last), data.py:161 / data.py:696 rooted-tail + # invariant, PPLNS window depth = min(height, REAL_CHAIN_LENGTH) / + # _pplns_max_shares = max(0, depth-1), pool_monitor.hpp:98 height>=10 gate. + # redistribute.hpp / pool_monitor.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_get_height_and_last_endpoints_test get_height_and_last_endpoints_test.cpp) + target_link_libraries(dgb_get_height_and_last_endpoints_test PRIVATE + GTest::gtest_main GTest::gtest + core + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage) + gtest_add_tests(dgb_get_height_and_last_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/get_height_and_last_endpoints_test.cpp b/src/impl/dgb/test/get_height_and_last_endpoints_test.cpp new file mode 100644 index 000000000..6285d89c6 --- /dev/null +++ b/src/impl/dgb/test/get_height_and_last_endpoints_test.cpp @@ -0,0 +1,114 @@ +// dgb get_height_and_last endpoints — chain-walk window arithmetic KAT. +// +// FENCED, additive (no production code touched this slice). Pins +// src/impl/dgb/coin/get_height_and_last_endpoints.hpp against the +// p2pool-dgb-scrypt oracle that governs every consumer of +// forest.get_height_and_last(): +// util/forest.py:171-173 get_height_and_last -> (delta.height, delta.tail) +// data.py:160-161 assert height >= net.REAL_CHAIN_LENGTH or last is None +// data.py:695-696 if height < CHAIN_LENGTH + 1 and last is not None: raise +// pplns window depth = min(height, REAL_CHAIN_LENGTH); _pplns_max_shares = max(0, depth-1) +// pool_monitor.hpp:98 if (height < 10) return 0; +// +// Every expectation is hand-derived from the oracle formula and the DGB net +// constants (REAL_CHAIN_LENGTH = 12*60*60//15 = 2880 mainnet / 400 testnet, +// MONITOR_MIN_HEIGHT = 10), NOT read from the code under test. This header lifts +// only the integer window guards over the already-resolved (height, last) pair; +// the get_delta_to_last skip-list walk stays in the forest. redistribute.hpp / +// pool_monitor.hpp / share_check.hpp are NOT rewired (delegation is the +// byte-identity follow-on). MUST appear in BOTH this dir CMakeLists.txt AND the +// build.yml --target allowlist, or it becomes a #143 NOT_BUILT sentinel. + +#include + +#include + +using namespace dgb; + +static constexpr int32_t CL_MAIN = 2880; // REAL_CHAIN_LENGTH mainnet +static constexpr int32_t CL_TEST = 400; // REAL_CHAIN_LENGTH testnet + +// --- ROOTED-TAIL invariant: height >= REAL_CHAIN_LENGTH or last is None ----- +TEST(DgbGhalEndpoints, RootedTailInvariant) { + // Fully rooted: deep chain with a concrete tail -> holds regardless of last. + EXPECT_TRUE(rooted_tail_invariant_holds(CL_MAIN, /*last_is_null=*/false, CL_MAIN)); + EXPECT_TRUE(rooted_tail_invariant_holds(CL_MAIN + 1, false, CL_MAIN)); + // Shallow but unrooted (last == None) -> holds. + EXPECT_TRUE(rooted_tail_invariant_holds(5, /*last_is_null=*/true, CL_MAIN)); + EXPECT_TRUE(rooted_tail_invariant_holds(0, true, CL_MAIN)); + // Shallow AND rooted (last present) -> VIOLATION (the oracle assert fires). + EXPECT_FALSE(rooted_tail_invariant_holds(5, /*last_is_null=*/false, CL_MAIN)); + EXPECT_FALSE(rooted_tail_invariant_holds(CL_MAIN - 1, false, CL_MAIN)); + // Boundary: exactly REAL_CHAIN_LENGTH is rooted (>=). + EXPECT_TRUE(rooted_tail_invariant_holds(CL_TEST, false, CL_TEST)); + EXPECT_FALSE(rooted_tail_invariant_holds(CL_TEST - 1, false, CL_TEST)); +} + +// --- attempt_verify entry guard: height >= CL+1 or last is None ------------- +TEST(DgbGhalEndpoints, VerifyDepthGuard) { + // data.py:696 raises when (height < CHAIN_LENGTH+1) AND (last is not None). + // ok == NOT(raise). + EXPECT_FALSE(verify_depth_ok(CL_MAIN, /*last_is_null=*/false, CL_MAIN)); // CL < CL+1, rooted -> raise + EXPECT_TRUE(verify_depth_ok(CL_MAIN + 1, false, CL_MAIN)); // height == CL+1 -> ok + EXPECT_TRUE(verify_depth_ok(3, /*last_is_null=*/true, CL_MAIN)); // unrooted -> ok at any depth + EXPECT_FALSE(verify_depth_ok(3, false, CL_MAIN)); // shallow + rooted -> raise + // Boundary at CHAIN_LENGTH+1 exactly (testnet). + EXPECT_TRUE(verify_depth_ok(CL_TEST + 1, false, CL_TEST)); + EXPECT_FALSE(verify_depth_ok(CL_TEST, false, CL_TEST)); +} + +// --- PPLNS window depth = min(height, REAL_CHAIN_LENGTH) --------------------- +TEST(DgbGhalEndpoints, PplnsWindowDepthClamps) { + EXPECT_EQ(pplns_window_depth(100, CL_MAIN), 100); // below clamp -> identity + EXPECT_EQ(pplns_window_depth(CL_MAIN, CL_MAIN), CL_MAIN); // at clamp + EXPECT_EQ(pplns_window_depth(CL_MAIN + 5000, CL_MAIN), CL_MAIN); // above -> clamped + EXPECT_EQ(pplns_window_depth(0, CL_MAIN), 0); + EXPECT_EQ(pplns_window_depth(450, CL_TEST), CL_TEST); // testnet clamp 400 + EXPECT_EQ(pplns_window_depth(399, CL_TEST), 399); +} + +// --- PPLNS window activation = depth >= 1 ----------------------------------- +TEST(DgbGhalEndpoints, PplnsWindowActivation) { + EXPECT_FALSE(pplns_window_active(0)); // redistribute.hpp:454 early-return + EXPECT_TRUE(pplns_window_active(1)); + EXPECT_TRUE(pplns_window_active(CL_MAIN)); + EXPECT_FALSE(pplns_window_active(-3)); // defensive: never active below 1 +} + +// --- p2pool _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) --- +TEST(DgbGhalEndpoints, PplnsMaxSharesMatchesOracle) { + EXPECT_EQ(pplns_max_shares(0, CL_MAIN), 0); // max(0, 0-1) = 0 + EXPECT_EQ(pplns_max_shares(1, CL_MAIN), 0); // max(0, 1-1) = 0 + EXPECT_EQ(pplns_max_shares(2, CL_MAIN), 1); // max(0, 2-1) = 1 + EXPECT_EQ(pplns_max_shares(100, CL_MAIN), 99); + EXPECT_EQ(pplns_max_shares(CL_MAIN, CL_MAIN), CL_MAIN - 1); // 2879 + EXPECT_EQ(pplns_max_shares(CL_MAIN + 1000, CL_MAIN), CL_MAIN - 1); // clamped then -1 + EXPECT_EQ(pplns_max_shares(CL_TEST, CL_TEST), CL_TEST - 1); // 399 +} + +// --- pool-monitor diagnostic gate: height >= 10 ----------------------------- +TEST(DgbGhalEndpoints, MonitorCycleGate) { + EXPECT_FALSE(monitor_cycle_runs(0)); + EXPECT_FALSE(monitor_cycle_runs(9)); // pool_monitor.hpp:98 boundary -> no-op + EXPECT_TRUE(monitor_cycle_runs(10)); // exactly the floor runs + EXPECT_TRUE(monitor_cycle_runs(2880)); +} + +// --- non-circular cross-check: window depth, max_shares, and activation are +// mutually consistent across the whole 0..2*CL range (derived purely from +// the oracle min/clamp identities, no code-under-test constants). --------- +TEST(DgbGhalEndpoints, WindowConsistencyNonCircular) { + for (int32_t h = 0; h <= 2 * CL_MAIN; ++h) { + int32_t depth = pplns_window_depth(h, CL_MAIN); + // depth never exceeds the clamp, never below 0 + ASSERT_LE(depth, CL_MAIN); + ASSERT_GE(depth, 0); + // expected clamp recomputed independently + int32_t expect_depth = (h < CL_MAIN) ? h : CL_MAIN; + ASSERT_EQ(depth, expect_depth); + // max_shares is exactly depth-1 floored at 0 + ASSERT_EQ(pplns_max_shares(h, CL_MAIN), (expect_depth > 0 ? expect_depth - 1 : 0)); + // active iff there is at least one share in range + ASSERT_EQ(pplns_window_active(depth), expect_depth >= 1); + } +}