Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
87 changes: 87 additions & 0 deletions src/impl/dgb/auto_ratchet_tail_guard.hpp
Original file line number Diff line number Diff line change
@@ -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:
// <stay on old format> # 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 <core/uint256.hpp> // uint288 work-weight accumulator
#include <cstdint>
#include <utility>

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 <typename WeightMap>
inline std::pair<typename WeightMap::mapped_type, typename WeightMap::mapped_type>
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<int64_t>(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 <typename W>
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<uint32_t>(switch_threshold))
/ W(static_cast<uint64_t>(100));
return !(target_weight < floor_gate);
}

// Convenience: reduce a weight map and apply the canonical switch gate.
template <typename WeightMap>
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
16 changes: 16 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
147 changes: 147 additions & 0 deletions src/impl/dgb/test/auto_ratchet_tail_guard_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>

#include <impl/dgb/auto_ratchet_tail_guard.hpp>
#include <core/uint256.hpp>

#include <cstdint>
#include <map>
#include <utility>

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<uint32_t>(100)) <
(total * static_cast<uint32_t>(thr)));
}

} // namespace

// --- reduce_target_total mirrors the inline accumulation -------------------
TEST(AutoRatchetTailGuard, ReduceSplitsAtTargetVersion)
{
std::map<uint64_t, uint288> 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<uint64_t, uint288> 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<uint64_t, uint288> 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<uint64_t, uint288> 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<uint32_t>(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;
}
}
Loading