Skip to content

Commit 95d5379

Browse files
committed
dgb(Phase-B): SSOT think() Phase-5 best-share punish walk + non-circular KAT
Lift the open-coded think() Phase-5 best-share resolution from share_tracker.hpp into a header-only SSOT (think_p5_best_share_punish.hpp): walk back off a naughty decorated head, dive to the deepest non-naughty descendant (skipping naughty children, 20-gen bound), and report the punishment of the share finally landed on, per p2pool data.py:2142-2166. FENCED/additive: dgb tree only, share_tracker.hpp NOT yet rewired (the byte-identity delegation is the follow-on). Pure graph traversal, consensus value semantics unchanged. KAT 6/6 with hand-derived non-circular fake-chain anchors; registered in CMake and both build.yml --target allowlist sites (#143 NOT_BUILT guard).
1 parent 27bd1a6 commit 95d5379

4 files changed

Lines changed: 266 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 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 \
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 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_p5_best_share_punish_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 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 \
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 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_p5_best_share_punish_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
220220
v37_test \
221221
-j$(nproc)
222222

src/impl/dgb/test/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,4 +690,19 @@ if (BUILD_TESTING AND GTest_FOUND)
690690
dgb_coin pool sharechain)
691691
gtest_add_tests(dgb_think_p3_best_head_test "" AUTO)
692692

693+
# dgb_think_p5_best_share_punish_test: FENCED, additive KAT pinning the
694+
# think() Phase-5 best-share punish walk in think_p5_best_share_punish.hpp
695+
# vs the p2pool data.py:2142-2166 best-share resolution oracle (walk back off
696+
# naughty heads, dive to deepest non-naughty descendant, report punishment),
697+
# plus hand-derived non-circular fake-chain anchors. Pure graph traversal ->
698+
# links only core. MUST also be in the build.yml --target allowlist (#143
699+
# NOT_BUILT trap).
700+
add_executable(dgb_think_p5_best_share_punish_test think_p5_best_share_punish_test.cpp)
701+
target_link_libraries(dgb_think_p5_best_share_punish_test PRIVATE
702+
GTest::gtest_main GTest::gtest
703+
core dgb
704+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
705+
dgb_coin pool sharechain)
706+
gtest_add_tests(dgb_think_p5_best_share_punish_test "" AUTO)
707+
693708
endif()
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// dgb think() Phase-5 BEST-SHARE PUNISH WALK conformance KAT.
2+
//
3+
// FENCED, additive (no production code touched this slice). Pins
4+
// src/impl/dgb/think_p5_best_share_punish.hpp against the p2pool data.py
5+
// think() best-share resolution oracle (data.py:2142-2166): walk back off a
6+
// naughty head, then dive to the deepest non-naughty descendant; report the
7+
// punishment of the share finally landed on.
8+
//
9+
// Expectations are HAND-DERIVED by tracing the walk rules over a fake chain,
10+
// NOT read back from the code under test — a conformance KAT that asks its
11+
// subject for the answer passes vacuously. Pure graph traversal: links only
12+
// core (no chain standup). MUST appear in BOTH this dir s CMakeLists.txt AND the
13+
// build.yml --target allowlist, or it becomes a #143 NOT_BUILT sentinel
14+
// (compiled-out, silently "passing").
15+
16+
#include <impl/dgb/think_p5_best_share_punish.hpp>
17+
18+
#include <gtest/gtest.h>
19+
20+
#include <map>
21+
#include <string>
22+
#include <vector>
23+
24+
namespace {
25+
26+
// A hand-built fake sharechain keyed by short string hashes. "" is the null
27+
// sentinel. Each node carries a naughty count and a prev pointer; children are
28+
// derived as the reverse of prev.
29+
struct FakeChain {
30+
std::map<std::string, int32_t> naughty; // present => contained
31+
std::map<std::string, std::string> prev; // hash -> prev hash ("" = none)
32+
33+
dgb::P5ChainAccessors<std::string> accessors() const {
34+
dgb::P5ChainAccessors<std::string> a;
35+
a.naughty = [this](const std::string& h) -> int32_t {
36+
auto it = naughty.find(h);
37+
return it == naughty.end() ? 0 : it->second;
38+
};
39+
a.prev_of = [this](const std::string& h) -> std::string {
40+
auto it = prev.find(h);
41+
return it == prev.end() ? std::string() : it->second;
42+
};
43+
a.contains = [this](const std::string& h) -> bool {
44+
return naughty.find(h) != naughty.end();
45+
};
46+
a.children = [this](const std::string& h) -> std::vector<std::string> {
47+
std::vector<std::string> kids;
48+
for (const auto& [k, p] : prev)
49+
if (p == h) kids.push_back(k);
50+
return kids;
51+
};
52+
a.is_null = [](const std::string& h) -> bool { return h.empty(); };
53+
return a;
54+
}
55+
};
56+
57+
// ---- (1) a non-naughty head is its own best, punish 0 ----------------------
58+
TEST(ThinkP5BestSharePunish, NonNaughtyHeadIsOwnBest)
59+
{
60+
FakeChain c;
61+
c.naughty = {{"G", 0}, {"M", 0}};
62+
c.prev = {{"M", "G"}, {"G", ""}};
63+
auto r = dgb::think_p5_best_share_punish<std::string>("M", true, c.accessors());
64+
EXPECT_EQ(r.best, "M"); // loop never entered
65+
EXPECT_EQ(r.punish_val, 0);
66+
}
67+
68+
// ---- (2) start_valid == false is returned verbatim, walk skipped -----------
69+
TEST(ThinkP5BestSharePunish, InvalidStartReturnedVerbatim)
70+
{
71+
FakeChain c;
72+
c.naughty = {{"Z", 4}}; // naughty data present but must be ignored
73+
c.prev = {{"Z", ""}};
74+
auto r = dgb::think_p5_best_share_punish<std::string>("Z", false, c.accessors());
75+
EXPECT_EQ(r.best, "Z");
76+
EXPECT_EQ(r.punish_val, 0);
77+
}
78+
79+
// ---- (3) walk back off naughty heads, dive to deepest non-naughty descendant
80+
// Chain: G(0) -> A(3) -> X(2)=start
81+
// G(0) -> C(0) -> D(0) -> E(0, leaf)
82+
// Trace: X naughty -> A naughty -> G non-naughty; dive from G: child A skipped
83+
// (naughty), child C -> D -> E gives the deepest non-naughty chain => best = E.
84+
// idx rests on G (naughty 0) => punish 0. Hand-derived, independent of subject.
85+
TEST(ThinkP5BestSharePunish, WalkBackThenDiveToDeepestDescendant)
86+
{
87+
FakeChain c;
88+
c.naughty = {{"G", 0}, {"A", 3}, {"X", 2}, {"C", 0}, {"D", 0}, {"E", 0}};
89+
c.prev = {{"G", ""}, {"A", "G"}, {"X", "A"}, {"C", "G"}, {"D", "C"}, {"E", "D"}};
90+
auto r = dgb::think_p5_best_share_punish<std::string>("X", true, c.accessors());
91+
EXPECT_EQ(r.best, "E");
92+
EXPECT_EQ(r.punish_val, 0);
93+
}
94+
95+
// ---- (4) the dive skips naughty children -----------------------------------
96+
// As (3) but C also has a DEEPER naughty branch C -> F(7) -> H(0). H is deeper
97+
// than E, but F is naughty so the whole branch is excluded; best is still E.
98+
TEST(ThinkP5BestSharePunish, DiveSkipsNaughtyChildren)
99+
{
100+
FakeChain c;
101+
c.naughty = {{"G", 0}, {"A", 3}, {"X", 2}, {"C", 0}, {"D", 0}, {"E", 0},
102+
{"F", 7}, {"H", 0}};
103+
c.prev = {{"G", ""}, {"A", "G"}, {"X", "A"}, {"C", "G"}, {"D", "C"},
104+
{"E", "D"}, {"F", "C"}, {"H", "F"}};
105+
auto r = dgb::think_p5_best_share_punish<std::string>("X", true, c.accessors());
106+
EXPECT_EQ(r.best, "E"); // C->F->H excluded (F naughty); C->D->E wins
107+
EXPECT_EQ(r.punish_val, 0);
108+
}
109+
110+
// ---- (5) naughty all the way to a missing prev -> stop, report punishment ---
111+
// Chain: Q(absent) <- P(5)=start. prev(P)=Q but Q is NOT contained, so the
112+
// walk breaks ON P. idx still points at P => punish = P naughty = 5.
113+
TEST(ThinkP5BestSharePunish, NaughtyToMissingPrevReportsPunish)
114+
{
115+
FakeChain c;
116+
c.naughty = {{"P", 5}}; // Q deliberately absent
117+
c.prev = {{"P", "Q"}};
118+
auto r = dgb::think_p5_best_share_punish<std::string>("P", true, c.accessors());
119+
EXPECT_EQ(r.best, "P");
120+
EXPECT_EQ(r.punish_val, 5);
121+
}
122+
123+
// ---- (6) naughty head with a null prev -> stop ON it, report punishment -----
124+
TEST(ThinkP5BestSharePunish, NaughtyWithNullPrevReportsPunish)
125+
{
126+
FakeChain c;
127+
c.naughty = {{"P", 2}};
128+
c.prev = {{"P", ""}}; // null prev sentinel
129+
auto r = dgb::think_p5_best_share_punish<std::string>("P", true, c.accessors());
130+
EXPECT_EQ(r.best, "P");
131+
EXPECT_EQ(r.punish_val, 2);
132+
}
133+
134+
} // namespace
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#pragma once
2+
3+
// SSOT for the think() Phase-5 BEST-SHARE PUNISH WALK — the pure graph decision
4+
// that, given the highest-scored decorated head, resolves the share the node
5+
// will actually build on and the punishment value it reports.
6+
//
7+
// p2pool punishes a share whose block was found invalid (naughty > 0): such a
8+
// head must NOT be built on directly. The walk is:
9+
// 1. start at the best decorated head;
10+
// 2. while it is naughty, step to its previous share; the moment a NON-naughty
11+
// ancestor is reached, dive forward to the DEEPEST non-naughty descendant
12+
// (skipping naughty children, bounded to 20 generations) and stop;
13+
// 3. if the walk runs off the end of the chain (prev missing) while still
14+
// naughty, stop on that last naughty share;
15+
// 4. punish_val = the naughty count of the share best_idx finally points at
16+
// (0 unless we stopped ON a naughty share).
17+
//
18+
// This is currently OPEN-CODED inline in share_tracker.hpp think() Phase 5. A
19+
// silent drift here changes which share the pool extends after an invalid block:
20+
// too-eager and we orphan good work; too-lax and we build on a known-bad head.
21+
// No compile error would catch a flipped naughty test or an off-by-one descend
22+
// bound. Lifting the walk to a header-only SSOT lets a KAT pin it on a fake
23+
// chain with no ShareTracker/NodeImpl standup.
24+
//
25+
// Oracle: p2pool data.py:2142-2166 OkayTracker.think() best-share resolution
26+
// (the `while punish ... self.heads ... best_descendent` walk-back loop).
27+
//
28+
// Per-coin isolation: dgb/ only. Header-only, additive; this slice does NOT yet
29+
// rewire share_tracker.hpp (that is the byte-identity-fenced delegation
30+
// follow-on) — it pins the walk as a free function so the KAT exercises it on a
31+
// hand-built graph. Consensus-neutral: pure graph traversal, no value semantics
32+
// changed.
33+
34+
#include <cstdint>
35+
#include <functional>
36+
#include <utility>
37+
#include <vector>
38+
39+
namespace dgb {
40+
41+
// Graph accessors over the sharechain, supplied by the caller (production wires
42+
// these to chain.get_index/get_share/contains/get_reverse; the KAT wires them to
43+
// hand-built maps). All are pure reads.
44+
template <typename Hash>
45+
struct P5ChainAccessors {
46+
// naughty count of a share (0 if non-naughty OR absent — mirrors the inline
47+
// `best_idx && best_idx->naughty > 0` guard, where a null index reads as 0).
48+
std::function<int32_t(const Hash&)> naughty;
49+
// m_prev_hash of a share (a null/sentinel Hash if none).
50+
std::function<Hash(const Hash&)> prev_of;
51+
// whether the chain contains a hash.
52+
std::function<bool(const Hash&)> contains;
53+
// reverse-map children of a share (empty if leaf/absent).
54+
std::function<std::vector<Hash>(const Hash&)> children;
55+
// whether a Hash is the null/sentinel value.
56+
std::function<bool(const Hash&)> is_null;
57+
};
58+
59+
template <typename Hash>
60+
struct P5Result {
61+
Hash best; // the share to build on
62+
int32_t punish_val; // self.punish reported out of Phase 5
63+
};
64+
65+
// Deepest non-naughty descendant from `h`, bounded to `limit` generations,
66+
// skipping naughty children. Returns {generations_below_h, deepest_hash}.
67+
// Byte-faithful to the inline `best_desc` lambda (first-strictly-greater wins;
68+
// a node with no eligible kids returns {0, h}).
69+
template <typename Hash>
70+
inline std::pair<int, Hash> think_p5_best_descendant(
71+
const Hash& h, int limit, const P5ChainAccessors<Hash>& acc)
72+
{
73+
if (limit < 0) return {0, h};
74+
auto kids = acc.children(h);
75+
if (kids.empty()) return {0, h};
76+
std::pair<int, Hash> best_kid = {-1, h};
77+
for (const auto& child : kids) {
78+
if (acc.naughty(child) > 0) continue;
79+
auto [gen, hash] = think_p5_best_descendant(child, limit - 1, acc);
80+
if (gen + 1 > best_kid.first) best_kid = {gen + 1, hash};
81+
}
82+
return best_kid.first >= 0 ? best_kid : std::pair<int, Hash>{0, h};
83+
}
84+
85+
// The Phase-5 best-share punish walk. `start` is the highest-scored decorated
86+
// head; `start_valid` is the outer `!best.IsNull() && chain.contains(best)`
87+
// guard — when false, the walk is skipped and {start, 0} is returned verbatim.
88+
template <typename Hash>
89+
inline P5Result<Hash> think_p5_best_share_punish(
90+
const Hash& start, bool start_valid, const P5ChainAccessors<Hash>& acc)
91+
{
92+
Hash best = start;
93+
if (!start_valid)
94+
return {best, 0};
95+
96+
// idx tracks the share best_idx points at; it can diverge from `best` once
97+
// we dive to a descendant (the inline code does NOT re-read best_idx there).
98+
Hash idx = best;
99+
if (acc.naughty(idx) > 0) {
100+
while (acc.naughty(idx) > 0) {
101+
Hash prev = acc.prev_of(best);
102+
if (acc.is_null(prev) || !acc.contains(prev)) break;
103+
best = prev;
104+
idx = prev;
105+
if (acc.naughty(idx) == 0) {
106+
best = think_p5_best_descendant(best, 20, acc).second;
107+
break;
108+
}
109+
}
110+
}
111+
int32_t punish_val = acc.naughty(idx) > 0 ? acc.naughty(idx) : 0;
112+
return {best, punish_val};
113+
}
114+
115+
} // namespace dgb

0 commit comments

Comments
 (0)