Skip to content

Commit 09dab20

Browse files
committed
dgb(phase-b): get_height_and_last endpoint window SSOT + KAT
Lift the pure integer window arithmetic that every consumer of forest.get_height_and_last() applies to the resolved (height, last) pair into a fenced header-only SSOT, coin/get_height_and_last_endpoints.hpp: - rooted-tail invariant = height >= REAL_CHAIN_LENGTH or last is None - verify entry guard = height >= CHAIN_LENGTH+1 or last is None - PPLNS window depth = min(height, REAL_CHAIN_LENGTH) - PPLNS window activation = depth >= 1 - PPLNS max shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) - monitor diagnostic gate = height >= 10 vs the p2pool-dgb-scrypt oracle: util/forest.py:171-173 (get_height_and_last -> delta.height, delta.tail), 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. The get_delta_to_last skip-list walk stays in the forest; this slice lifts only the integer guards over the already-resolved pair, with a non-circular KAT (7/7) that re-derives every expectation from the oracle min/clamp identities and DGB net constants, including a full 0..2*CHAIN_LENGTH consistency sweep. FENCED, additive: redistribute.hpp / pool_monitor.hpp / share_check.hpp NOT rewired (byte-identity delegation is the follow-on). Per-coin isolation: src/impl/dgb/ only. Test in both build.yml --target arms + dir CMakeLists.txt.
1 parent c2815b7 commit 09dab20

4 files changed

Lines changed: 245 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ jobs:
8484
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 \
8585
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 \
8686
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
87-
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 \
87+
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_get_height_and_last_endpoints_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test v37_test \
8888
-j$(nproc)
8989
9090
- name: Run tests
@@ -216,7 +216,7 @@ jobs:
216216
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 \
217217
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 \
218218
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
219-
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 \
219+
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_get_height_and_last_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 \
220220
v37_test \
221221
-j$(nproc)
222222
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#pragma once
2+
3+
// SSOT for the DGB GET_HEIGHT_AND_LAST chain-walk endpoints -- the pure integer
4+
// arithmetic that every consumer of forest.get_height_and_last() applies to the
5+
// resolved (height, last) pair before walking the sharechain. forest returns
6+
//
7+
// (delta.height, delta.tail) [p2pool util/forest.py:171-173]
8+
//
9+
// where `last` (delta.tail) is the hash one past the deepest reachable ancestor,
10+
// or null when the walk bottoms out at a genesis/rooted tail. Two facts about
11+
// that pair drive consensus-bearing windowing across the DGB tree:
12+
//
13+
// 1. The ROOTED-TAIL invariant. A verified head is only fully rooted once it
14+
// has >= REAL_CHAIN_LENGTH ancestors; below that it must still be hanging
15+
// off an unrooted tail (last is None). p2pool asserts this at every window
16+
// entry point:
17+
// data.py:161 assert height >= net.REAL_CHAIN_LENGTH or last is None
18+
// data.py:696 if height < CHAIN_LENGTH + 1 and last is not None: raise
19+
//
20+
// 2. The PPLNS / payout WINDOW DEPTH. The trailing payout walk is clamped to
21+
// the configured chain length and is only live once at least one share is
22+
// in range:
23+
// depth = min(height, REAL_CHAIN_LENGTH) (redistribute.hpp:453)
24+
// active = depth >= 1
25+
// which mirrors the oracle PPLNS bound
26+
// _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1)
27+
// (see pplns_weight_walk.hpp:96 / p2pool get_chain(best, min(height, CL))).
28+
//
29+
// 3. The MONITOR min-height gate. The pool monitor's diagnostic cycle is a
30+
// no-op until the chain is deep enough to be statistically meaningful:
31+
// if (height < 10) return 0; (pool_monitor.hpp:98)
32+
// Diagnostic-only, but pinned here so a silent drift of the floor is caught.
33+
//
34+
// A silent drift in the REAL_CHAIN_LENGTH clamp, the >=1 activation, or the
35+
// rooted-tail invariant would re-window PPLNS payout with NO compile error --
36+
// diverging operator-facing reward distribution from the p2pool-dgb-scrypt
37+
// reference the V36 master-compat invariant pins.
38+
//
39+
// Oracle: p2pool-dgb-scrypt util/forest.py:171-173 (get_height_and_last) +
40+
// data.py:160-161 (generate_transaction rooted-tail assert) +
41+
// data.py:695-696 (attempt_verify rooted-tail raise) +
42+
// networks/digibyte.py (REAL_CHAIN_LENGTH = 12*60*60//15 = 2880).
43+
//
44+
// Per-coin isolation: dgb/ only. Header-only, additive, free functions over the
45+
// already-resolved (height, last) pair -- the get_delta_to_last skip-list walk
46+
// stays in the forest. This slice does NOT rewire redistribute.hpp / pool_monitor.hpp
47+
// / share_check.hpp -- that is the byte-identity delegation follow-on. The lifted
48+
// bodies are verbatim copies of the inline guards (same int32_t height type,
49+
// same min()/comparison), so the follow-on is provably value-identical.
50+
// Consensus-neutral: pure arithmetic, no value semantics changed.
51+
52+
#include <algorithm>
53+
#include <cstdint>
54+
55+
namespace dgb {
56+
57+
// Minimum sharechain depth before the pool monitor's diagnostic cycle runs.
58+
// p2pool/c2pool pool_monitor.hpp:98 -- diagnostic floor, not consensus.
59+
static constexpr int32_t MONITOR_MIN_HEIGHT = 10;
60+
61+
// ROOTED-TAIL invariant (data.py:161, data.py:696). A resolved head must either
62+
// have at least `real_chain_length` ancestors, or still be hanging off an
63+
// unrooted tail (last is null). Returns true when the invariant holds.
64+
// p2pool: height >= net.REAL_CHAIN_LENGTH or last is None
65+
inline bool rooted_tail_invariant_holds(int32_t height,
66+
bool last_is_null,
67+
int32_t real_chain_length)
68+
{
69+
return height >= real_chain_length || last_is_null;
70+
}
71+
72+
// attempt_verify entry guard (data.py:695-696): a share below CHAIN_LENGTH+1
73+
// height that is NOT on an unrooted tail is malformed and must be rejected.
74+
// Returns true when the share is acceptable for verification at this depth.
75+
// p2pool: if height < CHAIN_LENGTH + 1 and last is not None: raise
76+
inline bool verify_depth_ok(int32_t height, bool last_is_null, int32_t chain_length)
77+
{
78+
return height >= chain_length + 1 || last_is_null;
79+
}
80+
81+
// PPLNS / payout window depth (redistribute.hpp:453). Trailing payout walk is
82+
// clamped to the configured real chain length.
83+
// c2pool: depth = min(height, real_chain_length())
84+
inline int32_t pplns_window_depth(int32_t height, int32_t real_chain_length)
85+
{
86+
return std::min(height, real_chain_length);
87+
}
88+
89+
// PPLNS window activation (redistribute.hpp:454). The payout window is only live
90+
// once at least one share is in range.
91+
// c2pool: if (depth < 1) return; -> active iff depth >= 1
92+
inline bool pplns_window_active(int32_t depth)
93+
{
94+
return depth >= 1;
95+
}
96+
97+
// p2pool PPLNS share-count bound (pplns_weight_walk.hpp:96): the number of
98+
// shares actually contributing to PPLNS weight, one less than the window depth
99+
// (the head itself is excluded), floored at zero.
100+
// p2pool: _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1)
101+
inline int32_t pplns_max_shares(int32_t height, int32_t real_chain_length)
102+
{
103+
return std::max(0, pplns_window_depth(height, real_chain_length) - 1);
104+
}
105+
106+
// Pool-monitor diagnostic gate (pool_monitor.hpp:98).
107+
// c2pool: if (height < 10) return 0; -> runs iff height >= 10
108+
inline bool monitor_cycle_runs(int32_t height)
109+
{
110+
return height >= MONITOR_MIN_HEIGHT;
111+
}
112+
113+
} // namespace dgb

src/impl/dgb/test/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,22 @@ if (BUILD_TESTING AND GTest_FOUND)
708708
target_link_libraries(dgb_expected_time_to_block_test PRIVATE
709709
GTest::gtest_main GTest::gtest)
710710
gtest_add_tests(dgb_expected_time_to_block_test "" AUTO)
711+
712+
# dgb_get_height_and_last_endpoints_test: FENCED, additive KAT pinning the
713+
# get_height_and_last chain-walk WINDOW arithmetic in
714+
# coin/get_height_and_last_endpoints.hpp vs the p2pool-dgb-scrypt oracle:
715+
# forest.py:171-173 (height,last), data.py:161 / data.py:696 rooted-tail
716+
# invariant, PPLNS window depth = min(height, REAL_CHAIN_LENGTH) /
717+
# _pplns_max_shares = max(0, depth-1), pool_monitor.hpp:98 height>=10 gate.
718+
# redistribute.hpp / pool_monitor.hpp NOT yet rewired (delegation is the
719+
# byte-identity follow-on). MUST also be in the build.yml --target allowlist
720+
# (#143 NOT_BUILT trap).
721+
add_executable(dgb_get_height_and_last_endpoints_test get_height_and_last_endpoints_test.cpp)
722+
target_link_libraries(dgb_get_height_and_last_endpoints_test PRIVATE
723+
GTest::gtest_main GTest::gtest
724+
core
725+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
726+
gtest_add_tests(dgb_get_height_and_last_endpoints_test "" AUTO)
711727
# dgb_pool_efficiency_test: FENCED, additive KAT pinning the pool
712728
# EFFICIENCY / REAL-HASHRATE diagnostics arithmetic in pool_efficiency.hpp vs
713729
# the p2pool main.py status-loop oracle (stale_prop = (orphan+doa)/total;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// dgb get_height_and_last endpoints — chain-walk window arithmetic KAT.
2+
//
3+
// FENCED, additive (no production code touched this slice). Pins
4+
// src/impl/dgb/coin/get_height_and_last_endpoints.hpp against the
5+
// p2pool-dgb-scrypt oracle that governs every consumer of
6+
// forest.get_height_and_last():
7+
// util/forest.py:171-173 get_height_and_last -> (delta.height, delta.tail)
8+
// data.py:160-161 assert height >= net.REAL_CHAIN_LENGTH or last is None
9+
// data.py:695-696 if height < CHAIN_LENGTH + 1 and last is not None: raise
10+
// pplns window depth = min(height, REAL_CHAIN_LENGTH); _pplns_max_shares = max(0, depth-1)
11+
// pool_monitor.hpp:98 if (height < 10) return 0;
12+
//
13+
// Every expectation is hand-derived from the oracle formula and the DGB net
14+
// constants (REAL_CHAIN_LENGTH = 12*60*60//15 = 2880 mainnet / 400 testnet,
15+
// MONITOR_MIN_HEIGHT = 10), NOT read from the code under test. This header lifts
16+
// only the integer window guards over the already-resolved (height, last) pair;
17+
// the get_delta_to_last skip-list walk stays in the forest. redistribute.hpp /
18+
// pool_monitor.hpp / share_check.hpp are NOT rewired (delegation is the
19+
// byte-identity follow-on). MUST appear in BOTH this dir CMakeLists.txt AND the
20+
// build.yml --target allowlist, or it becomes a #143 NOT_BUILT sentinel.
21+
22+
#include <impl/dgb/coin/get_height_and_last_endpoints.hpp>
23+
24+
#include <gtest/gtest.h>
25+
26+
using namespace dgb;
27+
28+
static constexpr int32_t CL_MAIN = 2880; // REAL_CHAIN_LENGTH mainnet
29+
static constexpr int32_t CL_TEST = 400; // REAL_CHAIN_LENGTH testnet
30+
31+
// --- ROOTED-TAIL invariant: height >= REAL_CHAIN_LENGTH or last is None -----
32+
TEST(DgbGhalEndpoints, RootedTailInvariant) {
33+
// Fully rooted: deep chain with a concrete tail -> holds regardless of last.
34+
EXPECT_TRUE(rooted_tail_invariant_holds(CL_MAIN, /*last_is_null=*/false, CL_MAIN));
35+
EXPECT_TRUE(rooted_tail_invariant_holds(CL_MAIN + 1, false, CL_MAIN));
36+
// Shallow but unrooted (last == None) -> holds.
37+
EXPECT_TRUE(rooted_tail_invariant_holds(5, /*last_is_null=*/true, CL_MAIN));
38+
EXPECT_TRUE(rooted_tail_invariant_holds(0, true, CL_MAIN));
39+
// Shallow AND rooted (last present) -> VIOLATION (the oracle assert fires).
40+
EXPECT_FALSE(rooted_tail_invariant_holds(5, /*last_is_null=*/false, CL_MAIN));
41+
EXPECT_FALSE(rooted_tail_invariant_holds(CL_MAIN - 1, false, CL_MAIN));
42+
// Boundary: exactly REAL_CHAIN_LENGTH is rooted (>=).
43+
EXPECT_TRUE(rooted_tail_invariant_holds(CL_TEST, false, CL_TEST));
44+
EXPECT_FALSE(rooted_tail_invariant_holds(CL_TEST - 1, false, CL_TEST));
45+
}
46+
47+
// --- attempt_verify entry guard: height >= CL+1 or last is None -------------
48+
TEST(DgbGhalEndpoints, VerifyDepthGuard) {
49+
// data.py:696 raises when (height < CHAIN_LENGTH+1) AND (last is not None).
50+
// ok == NOT(raise).
51+
EXPECT_FALSE(verify_depth_ok(CL_MAIN, /*last_is_null=*/false, CL_MAIN)); // CL < CL+1, rooted -> raise
52+
EXPECT_TRUE(verify_depth_ok(CL_MAIN + 1, false, CL_MAIN)); // height == CL+1 -> ok
53+
EXPECT_TRUE(verify_depth_ok(3, /*last_is_null=*/true, CL_MAIN)); // unrooted -> ok at any depth
54+
EXPECT_FALSE(verify_depth_ok(3, false, CL_MAIN)); // shallow + rooted -> raise
55+
// Boundary at CHAIN_LENGTH+1 exactly (testnet).
56+
EXPECT_TRUE(verify_depth_ok(CL_TEST + 1, false, CL_TEST));
57+
EXPECT_FALSE(verify_depth_ok(CL_TEST, false, CL_TEST));
58+
}
59+
60+
// --- PPLNS window depth = min(height, REAL_CHAIN_LENGTH) ---------------------
61+
TEST(DgbGhalEndpoints, PplnsWindowDepthClamps) {
62+
EXPECT_EQ(pplns_window_depth(100, CL_MAIN), 100); // below clamp -> identity
63+
EXPECT_EQ(pplns_window_depth(CL_MAIN, CL_MAIN), CL_MAIN); // at clamp
64+
EXPECT_EQ(pplns_window_depth(CL_MAIN + 5000, CL_MAIN), CL_MAIN); // above -> clamped
65+
EXPECT_EQ(pplns_window_depth(0, CL_MAIN), 0);
66+
EXPECT_EQ(pplns_window_depth(450, CL_TEST), CL_TEST); // testnet clamp 400
67+
EXPECT_EQ(pplns_window_depth(399, CL_TEST), 399);
68+
}
69+
70+
// --- PPLNS window activation = depth >= 1 -----------------------------------
71+
TEST(DgbGhalEndpoints, PplnsWindowActivation) {
72+
EXPECT_FALSE(pplns_window_active(0)); // redistribute.hpp:454 early-return
73+
EXPECT_TRUE(pplns_window_active(1));
74+
EXPECT_TRUE(pplns_window_active(CL_MAIN));
75+
EXPECT_FALSE(pplns_window_active(-3)); // defensive: never active below 1
76+
}
77+
78+
// --- p2pool _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) ---
79+
TEST(DgbGhalEndpoints, PplnsMaxSharesMatchesOracle) {
80+
EXPECT_EQ(pplns_max_shares(0, CL_MAIN), 0); // max(0, 0-1) = 0
81+
EXPECT_EQ(pplns_max_shares(1, CL_MAIN), 0); // max(0, 1-1) = 0
82+
EXPECT_EQ(pplns_max_shares(2, CL_MAIN), 1); // max(0, 2-1) = 1
83+
EXPECT_EQ(pplns_max_shares(100, CL_MAIN), 99);
84+
EXPECT_EQ(pplns_max_shares(CL_MAIN, CL_MAIN), CL_MAIN - 1); // 2879
85+
EXPECT_EQ(pplns_max_shares(CL_MAIN + 1000, CL_MAIN), CL_MAIN - 1); // clamped then -1
86+
EXPECT_EQ(pplns_max_shares(CL_TEST, CL_TEST), CL_TEST - 1); // 399
87+
}
88+
89+
// --- pool-monitor diagnostic gate: height >= 10 -----------------------------
90+
TEST(DgbGhalEndpoints, MonitorCycleGate) {
91+
EXPECT_FALSE(monitor_cycle_runs(0));
92+
EXPECT_FALSE(monitor_cycle_runs(9)); // pool_monitor.hpp:98 boundary -> no-op
93+
EXPECT_TRUE(monitor_cycle_runs(10)); // exactly the floor runs
94+
EXPECT_TRUE(monitor_cycle_runs(2880));
95+
}
96+
97+
// --- non-circular cross-check: window depth, max_shares, and activation are
98+
// mutually consistent across the whole 0..2*CL range (derived purely from
99+
// the oracle min/clamp identities, no code-under-test constants). ---------
100+
TEST(DgbGhalEndpoints, WindowConsistencyNonCircular) {
101+
for (int32_t h = 0; h <= 2 * CL_MAIN; ++h) {
102+
int32_t depth = pplns_window_depth(h, CL_MAIN);
103+
// depth never exceeds the clamp, never below 0
104+
ASSERT_LE(depth, CL_MAIN);
105+
ASSERT_GE(depth, 0);
106+
// expected clamp recomputed independently
107+
int32_t expect_depth = (h < CL_MAIN) ? h : CL_MAIN;
108+
ASSERT_EQ(depth, expect_depth);
109+
// max_shares is exactly depth-1 floored at 0
110+
ASSERT_EQ(pplns_max_shares(h, CL_MAIN), (expect_depth > 0 ? expect_depth - 1 : 0));
111+
// active iff there is at least one share in range
112+
ASSERT_EQ(pplns_window_active(depth), expect_depth >= 1);
113+
}
114+
}

0 commit comments

Comments
 (0)