diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 889d80af8..836a9c408 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 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 \ 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 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_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 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 \ 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 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_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/auto_ratchet_tail_guard.hpp b/src/impl/dgb/auto_ratchet_tail_guard.hpp new file mode 100644 index 000000000..4dea761f6 --- /dev/null +++ b/src/impl/dgb/auto_ratchet_tail_guard.hpp @@ -0,0 +1,87 @@ +#pragma once + +// AutoRatchet tail-guard SSOT (DGB Phase-B conformance pillar). +// +// Single source of truth for the WORK-WEIGHTED 60% tail guard that +// AutoRatchet::get_share_version evaluates over the oldest 10% of the +// activation window [9/10*CHAIN_LENGTH, CHAIN_LENGTH] before it may transition +// VOTING -> ACTIVATED. This is the mint<->accept coupling that stops a +// 95%-by-COUNT activation from outrunning the 60%-by-WORK accept gate +// (share_check step 2 / p2pool check() data.py:1399): without it a node mints +// a V36 boundary share every mature peer rejects and the crossing wedges. +// +// Oracle: frstrtr/p2pool-dgb-scrypt data.py get_desired_version_counts weights +// each share by target_to_average_attempts(target) = WORK (data.py:2651), and +// the canonical switch rule (data.py:1399) reads: +// +// if counts.get(VERSION, 0) < sum(counts.itervalues()) * 60 // 100: +// # i.e. NOT switched +// +// i.e. SWITCHED iff target_weight >= floor(total_weight * 60 / 100). +// +// CONFORMANCE NOTE (divergence flagged to integrator/decisions 2026-06-24): +// the live inline in auto_ratchet.hpp computes the negation as +// tail_target * 100 < tail_total * 60 +// which is the EXACT-RATIONAL test target < total*60/100, NOT the canonical +// FLOOR test target < floor(total*60/100). The two differ in exactly one case +// -- target == floor(total*60/100) AND (total*60) % 100 != 0 -- where the +// inline waits one extra work-quantum (negligibly STRICTER; never observed +// with real uint288 work weights, but a formal divergence from the oracle). +// This SSOT implements the EXACT canonical floor expression so a future +// delegation makes the inline byte-faithful to the oracle. auto_ratchet.hpp is +// NOT rewired here (delegation is the byte-identity follow-on); this header is +// additive + FENCED (dgb-tree-local, consensus path untouched). + +#include // uint288 work-weight accumulator +#include +#include + +namespace dgb::auto_ratchet +{ + +inline constexpr int SWITCH_THRESHOLD = 60; // % work required to switch format + +// Reduce a desired-version -> work-weight map into +// {weight voting version >= target, total weight}. +// Mirrors the inline accumulation in AutoRatchet::get_share_version verbatim +// (uint288 default-initialises to 0; map iteration order is version-ascending +// but the sum is order-independent). +template +inline std::pair +reduce_target_total(const WeightMap& weights, int64_t target_version) +{ + using W = typename WeightMap::mapped_type; + W target{}, total{}; + for (const auto& kv : weights) { + total = total + kv.second; + if (static_cast(kv.first) >= target_version) + target = target + kv.second; + } + return {target, total}; +} + +// Canonical p2pool switch predicate (data.py:1399) with EXACT floor semantics: +// SWITCHED iff target_weight >= floor(total_weight * threshold / 100). +// Returns true when the work-weighted V>=target support meets the gate +// (== tail guard PASSES, AutoRatchet may proceed VOTING -> ACTIVATED). +template +inline bool switched(const W& target_weight, const W& total_weight, + int switch_threshold = SWITCH_THRESHOLD) +{ + // floor(total * thr / 100) on the unsigned work accumulator. base_uint + // multiply takes uint32_t; divide takes the same width, so wrap 100 in W. + W floor_gate = (total_weight * static_cast(switch_threshold)) + / W(static_cast(100)); + return !(target_weight < floor_gate); +} + +// Convenience: reduce a weight map and apply the canonical switch gate. +template +inline bool tail_guard_passes(const WeightMap& weights, int64_t target_version, + int switch_threshold = SWITCH_THRESHOLD) +{ + auto tt = reduce_target_total(weights, target_version); + return switched(tt.first, tt.second, switch_threshold); +} + +} // namespace dgb::auto_ratchet diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index a43045e63..65ee854de 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -702,6 +702,22 @@ if (BUILD_TESTING AND GTest_FOUND) GTest::gtest_main GTest::gtest) gtest_add_tests(dgb_pool_efficiency_test "" AUTO) + # dgb_auto_ratchet_tail_guard_test: FENCED, additive conformance KAT for the + # AutoRatchet WORK-WEIGHTED 60% tail-guard SSOT (auto_ratchet_tail_guard.hpp) + # vs the p2pool-dgb-scrypt switch rule data.py:1399 + # (SWITCHED iff target_weight >= floor(total_weight*60/100)). Non-circular: + # hand-derived oracle values + a verbatim replica of the live inline to pin + # the one floor-boundary divergence. uint288 arithmetic -> links core. + # auto_ratchet.hpp NOT rewired (delegation is the byte-id follow-on). MUST + # also be in the build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_auto_ratchet_tail_guard_test auto_ratchet_tail_guard_test.cpp) + target_link_libraries(dgb_auto_ratchet_tail_guard_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_auto_ratchet_tail_guard_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 # p2pool data.py think() oracle (selection key (adjusted_work,-reason,-ts) diff --git a/src/impl/dgb/test/auto_ratchet_tail_guard_test.cpp b/src/impl/dgb/test/auto_ratchet_tail_guard_test.cpp new file mode 100644 index 000000000..e4138af97 --- /dev/null +++ b/src/impl/dgb/test/auto_ratchet_tail_guard_test.cpp @@ -0,0 +1,147 @@ +// dgb_auto_ratchet_tail_guard_test: FENCED, additive conformance KAT for the +// AutoRatchet WORK-WEIGHTED 60% tail guard SSOT (auto_ratchet_tail_guard.hpp). +// +// Pins the canonical p2pool-dgb-scrypt switch rule (data.py:1399): +// SWITCHED iff target_weight >= floor(total_weight * 60 / 100), +// over uint288 work-weight accumulators, NON-CIRCULARLY (hand-derived oracle +// values, plus a verbatim replica of the live inline expression to localise +// the one floor-boundary divergence documented in the SSOT header). +// +// auto_ratchet.hpp consensus path is NOT exercised here -- this is a pure +// arithmetic pin of the lifted SSOT. Test-only / dgb-tree-local. + +#include + +#include +#include + +#include +#include +#include + +using dgb::auto_ratchet::reduce_target_total; +using dgb::auto_ratchet::switched; +using dgb::auto_ratchet::tail_guard_passes; + +namespace { + +// Verbatim replica of the LIVE inline expression in +// AutoRatchet::get_share_version: tail_ok = !(target*100 < total*60). +// Kept here so the boundary tests below compare SSOT-canonical-floor against +// the inline exact-rational form WITHOUT importing the consensus header. +bool inline_tail_ok(const uint288& target, const uint288& total, int thr = 60) +{ + return !((target * static_cast(100)) < + (total * static_cast(thr))); +} + +} // namespace + +// --- reduce_target_total mirrors the inline accumulation ------------------- +TEST(AutoRatchetTailGuard, ReduceSplitsAtTargetVersion) +{ + std::map w{ + {34, uint288(5)}, // below target -> total only + {35, uint288(7)}, // below target -> total only + {36, uint288(11)}, // >= target -> target + total + {37, uint288(13)}, // >= target -> target + total + }; + auto tt = reduce_target_total(w, /*target_version=*/36); + EXPECT_EQ(tt.first, uint288(24)); // 11 + 13 + EXPECT_EQ(tt.second, uint288(36)); // 5 + 7 + 11 + 13 +} + +TEST(AutoRatchetTailGuard, EmptyMapIsZeroZero) +{ + std::map w; + auto tt = reduce_target_total(w, 36); + EXPECT_EQ(tt.first, uint288(0)); + EXPECT_EQ(tt.second, uint288(0)); +} + +// --- canonical floor switch gate (oracle data.py:1399) --------------------- +TEST(AutoRatchetTailGuard, SwitchedHonoursSixtyPercentFloor) +{ + // total=100 -> floor(100*60/100)=60. SWITCHED iff target >= 60. + EXPECT_FALSE(switched(uint288(59), uint288(100))); + EXPECT_TRUE (switched(uint288(60), uint288(100))); + EXPECT_TRUE (switched(uint288(61), uint288(100))); + EXPECT_TRUE (switched(uint288(100), uint288(100))); +} + +TEST(AutoRatchetTailGuard, AllOldNeverSwitches) +{ + // No V>=target work at all: target=0, gate=floor(total*60/100) >= 1 for + // any total>=2 -> never switched. + EXPECT_FALSE(switched(uint288(0), uint288(2))); + EXPECT_FALSE(switched(uint288(0), uint288(1000))); +} + +TEST(AutoRatchetTailGuard, ZeroTotalSwitches) +{ + // Empty window: gate=floor(0)=0, target=0 >= 0 -> vacuously switched. + // Matches p2pool: 0 < 0 is False, so it does NOT stay on old format. + EXPECT_TRUE(switched(uint288(0), uint288(0))); +} + +// --- non-circular boundary: SSOT floor vs live inline exact-rational ------- +// Demonstrates the SOLE divergence documented in the SSOT header: +// target == floor(total*60/100) AND (total*60) % 100 != 0. +// total=7: total*60=420, floor(420/100)=4. +// SSOT canonical : target>=4 -> SWITCHED at target==4. +// live inline : 4*100=400 < 420 -> tail_ok=false (NOT switched) at 4. +TEST(AutoRatchetTailGuard, FloorBoundaryDivergesFromInlineByOneQuantum) +{ + const uint288 total(7); + + // Below the floor: both agree -> not switched. + EXPECT_FALSE(switched(uint288(3), total)); + EXPECT_FALSE(inline_tail_ok(uint288(3), total)); + + // AT the floor (=4): SSOT switches, inline still waits. The documented + // off-by-one-quantum divergence, pinned so a future delegation is a + // deliberate, reviewed change rather than a silent flip. + EXPECT_TRUE (switched(uint288(4), total)); // canonical: 4 >= floor(4.2)=4 + EXPECT_FALSE(inline_tail_ok(uint288(4), total)); // inline: 400 < 420 + + // Above the floor: both agree -> switched / tail_ok. + EXPECT_TRUE(switched(uint288(5), total)); + EXPECT_TRUE(inline_tail_ok(uint288(5), total)); +} + +// Where (total*60) % 100 == 0 the floor is exact and the two forms AGREE for +// every target -> the divergence really is confined to the non-exact case. +TEST(AutoRatchetTailGuard, ExactFloorAgreesWithInline) +{ + const uint288 total(100); // 100*60=6000, divisible by 100, floor exact. + for (uint32_t t = 55; t <= 65; ++t) { + const uint288 target(t); + EXPECT_EQ(switched(target, total), inline_tail_ok(target, total)) + << "target=" << t; + } +} + +// --- tail_guard_passes end-to-end over a weight map ------------------------ +TEST(AutoRatchetTailGuard, TailGuardPassesOnWorkWeightedMajority) +{ + // 70 work-units vote V36, 30 vote V35 -> 70 >= floor(100*60/100)=60 -> pass. + std::map pass{{35, uint288(30)}, {36, uint288(70)}}; + EXPECT_TRUE(tail_guard_passes(pass, 36)); + + // 55 vote V36, 45 vote V35 -> 55 < 60 -> guard holds (do NOT activate). + std::map wait{{35, uint288(45)}, {36, uint288(55)}}; + EXPECT_FALSE(tail_guard_passes(wait, 36)); +} + +// Large uint288 work weights (realistic target_to_average_attempts scale): +// SSOT and inline agree -- the boundary case is measure-zero in practice. +TEST(AutoRatchetTailGuard, LargeWeightsAgreeWithInline) +{ + const uint288 big = uint288(uint64_t(1) << 40); // ~1.1e12 work-units + const uint288 total = big * static_cast(100); + for (uint32_t pct : {0u, 1u, 59u, 60u, 61u, 99u, 100u}) { + const uint288 target = big * pct; + EXPECT_EQ(switched(target, total), inline_tail_ok(target, total)) + << "pct=" << pct; + } +}