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 \
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 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 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 \
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 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 test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
v37_test \
-j$(nproc)

Expand Down
15 changes: 15 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -726,4 +726,19 @@ if (BUILD_TESTING AND GTest_FOUND)
dgb_coin pool sharechain)
gtest_add_tests(dgb_think_p2_walk_bounds_test "" AUTO)

# dgb_think_p5_best_share_punish_test: FENCED, additive KAT pinning the
# think() Phase-5 best-share punish walk in think_p5_best_share_punish.hpp
# vs the p2pool data.py:2142-2166 best-share resolution oracle (walk back off
# naughty heads, dive to deepest non-naughty descendant, report punishment),
# plus hand-derived non-circular fake-chain anchors. Pure graph traversal ->
# links only core. MUST also be in the build.yml --target allowlist (#143
# NOT_BUILT trap).
add_executable(dgb_think_p5_best_share_punish_test think_p5_best_share_punish_test.cpp)
target_link_libraries(dgb_think_p5_best_share_punish_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_think_p5_best_share_punish_test "" AUTO)

endif()
134 changes: 134 additions & 0 deletions src/impl/dgb/test/think_p5_best_share_punish_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// dgb think() Phase-5 BEST-SHARE PUNISH WALK conformance KAT.
//
// FENCED, additive (no production code touched this slice). Pins
// src/impl/dgb/think_p5_best_share_punish.hpp against the p2pool data.py
// think() best-share resolution oracle (data.py:2142-2166): walk back off a
// naughty head, then dive to the deepest non-naughty descendant; report the
// punishment of the share finally landed on.
//
// Expectations are HAND-DERIVED by tracing the walk rules over a fake chain,
// NOT read back from the code under test — a conformance KAT that asks its
// subject for the answer passes vacuously. Pure graph traversal: links only
// core (no chain standup). MUST appear in BOTH this dir s CMakeLists.txt AND the
// build.yml --target allowlist, or it becomes a #143 NOT_BUILT sentinel
// (compiled-out, silently "passing").

#include <impl/dgb/think_p5_best_share_punish.hpp>

#include <gtest/gtest.h>

#include <map>
#include <string>
#include <vector>

namespace {

// A hand-built fake sharechain keyed by short string hashes. "" is the null
// sentinel. Each node carries a naughty count and a prev pointer; children are
// derived as the reverse of prev.
struct FakeChain {
std::map<std::string, int32_t> naughty; // present => contained
std::map<std::string, std::string> prev; // hash -> prev hash ("" = none)

dgb::P5ChainAccessors<std::string> accessors() const {
dgb::P5ChainAccessors<std::string> a;
a.naughty = [this](const std::string& h) -> int32_t {
auto it = naughty.find(h);
return it == naughty.end() ? 0 : it->second;
};
a.prev_of = [this](const std::string& h) -> std::string {
auto it = prev.find(h);
return it == prev.end() ? std::string() : it->second;
};
a.contains = [this](const std::string& h) -> bool {
return naughty.find(h) != naughty.end();
};
a.children = [this](const std::string& h) -> std::vector<std::string> {
std::vector<std::string> kids;
for (const auto& [k, p] : prev)
if (p == h) kids.push_back(k);
return kids;
};
a.is_null = [](const std::string& h) -> bool { return h.empty(); };
return a;
}
};

// ---- (1) a non-naughty head is its own best, punish 0 ----------------------
TEST(ThinkP5BestSharePunish, NonNaughtyHeadIsOwnBest)
{
FakeChain c;
c.naughty = {{"G", 0}, {"M", 0}};
c.prev = {{"M", "G"}, {"G", ""}};
auto r = dgb::think_p5_best_share_punish<std::string>("M", true, c.accessors());
EXPECT_EQ(r.best, "M"); // loop never entered
EXPECT_EQ(r.punish_val, 0);
}

// ---- (2) start_valid == false is returned verbatim, walk skipped -----------
TEST(ThinkP5BestSharePunish, InvalidStartReturnedVerbatim)
{
FakeChain c;
c.naughty = {{"Z", 4}}; // naughty data present but must be ignored
c.prev = {{"Z", ""}};
auto r = dgb::think_p5_best_share_punish<std::string>("Z", false, c.accessors());
EXPECT_EQ(r.best, "Z");
EXPECT_EQ(r.punish_val, 0);
}

// ---- (3) walk back off naughty heads, dive to deepest non-naughty descendant
// Chain: G(0) -> A(3) -> X(2)=start
// G(0) -> C(0) -> D(0) -> E(0, leaf)
// Trace: X naughty -> A naughty -> G non-naughty; dive from G: child A skipped
// (naughty), child C -> D -> E gives the deepest non-naughty chain => best = E.
// idx rests on G (naughty 0) => punish 0. Hand-derived, independent of subject.
TEST(ThinkP5BestSharePunish, WalkBackThenDiveToDeepestDescendant)
{
FakeChain c;
c.naughty = {{"G", 0}, {"A", 3}, {"X", 2}, {"C", 0}, {"D", 0}, {"E", 0}};
c.prev = {{"G", ""}, {"A", "G"}, {"X", "A"}, {"C", "G"}, {"D", "C"}, {"E", "D"}};
auto r = dgb::think_p5_best_share_punish<std::string>("X", true, c.accessors());
EXPECT_EQ(r.best, "E");
EXPECT_EQ(r.punish_val, 0);
}

// ---- (4) the dive skips naughty children -----------------------------------
// As (3) but C also has a DEEPER naughty branch C -> F(7) -> H(0). H is deeper
// than E, but F is naughty so the whole branch is excluded; best is still E.
TEST(ThinkP5BestSharePunish, DiveSkipsNaughtyChildren)
{
FakeChain c;
c.naughty = {{"G", 0}, {"A", 3}, {"X", 2}, {"C", 0}, {"D", 0}, {"E", 0},
{"F", 7}, {"H", 0}};
c.prev = {{"G", ""}, {"A", "G"}, {"X", "A"}, {"C", "G"}, {"D", "C"},
{"E", "D"}, {"F", "C"}, {"H", "F"}};
auto r = dgb::think_p5_best_share_punish<std::string>("X", true, c.accessors());
EXPECT_EQ(r.best, "E"); // C->F->H excluded (F naughty); C->D->E wins
EXPECT_EQ(r.punish_val, 0);
}

// ---- (5) naughty all the way to a missing prev -> stop, report punishment ---
// Chain: Q(absent) <- P(5)=start. prev(P)=Q but Q is NOT contained, so the
// walk breaks ON P. idx still points at P => punish = P naughty = 5.
TEST(ThinkP5BestSharePunish, NaughtyToMissingPrevReportsPunish)
{
FakeChain c;
c.naughty = {{"P", 5}}; // Q deliberately absent
c.prev = {{"P", "Q"}};
auto r = dgb::think_p5_best_share_punish<std::string>("P", true, c.accessors());
EXPECT_EQ(r.best, "P");
EXPECT_EQ(r.punish_val, 5);
}

// ---- (6) naughty head with a null prev -> stop ON it, report punishment -----
TEST(ThinkP5BestSharePunish, NaughtyWithNullPrevReportsPunish)
{
FakeChain c;
c.naughty = {{"P", 2}};
c.prev = {{"P", ""}}; // null prev sentinel
auto r = dgb::think_p5_best_share_punish<std::string>("P", true, c.accessors());
EXPECT_EQ(r.best, "P");
EXPECT_EQ(r.punish_val, 2);
}

} // namespace
115 changes: 115 additions & 0 deletions src/impl/dgb/think_p5_best_share_punish.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#pragma once

// SSOT for the think() Phase-5 BEST-SHARE PUNISH WALK — the pure graph decision
// that, given the highest-scored decorated head, resolves the share the node
// will actually build on and the punishment value it reports.
//
// p2pool punishes a share whose block was found invalid (naughty > 0): such a
// head must NOT be built on directly. The walk is:
// 1. start at the best decorated head;
// 2. while it is naughty, step to its previous share; the moment a NON-naughty
// ancestor is reached, dive forward to the DEEPEST non-naughty descendant
// (skipping naughty children, bounded to 20 generations) and stop;
// 3. if the walk runs off the end of the chain (prev missing) while still
// naughty, stop on that last naughty share;
// 4. punish_val = the naughty count of the share best_idx finally points at
// (0 unless we stopped ON a naughty share).
//
// This is currently OPEN-CODED inline in share_tracker.hpp think() Phase 5. A
// silent drift here changes which share the pool extends after an invalid block:
// too-eager and we orphan good work; too-lax and we build on a known-bad head.
// No compile error would catch a flipped naughty test or an off-by-one descend
// bound. Lifting the walk to a header-only SSOT lets a KAT pin it on a fake
// chain with no ShareTracker/NodeImpl standup.
//
// Oracle: p2pool data.py:2142-2166 OkayTracker.think() best-share resolution
// (the `while punish ... self.heads ... best_descendent` walk-back loop).
//
// Per-coin isolation: dgb/ only. Header-only, additive; this slice does NOT yet
// rewire share_tracker.hpp (that is the byte-identity-fenced delegation
// follow-on) — it pins the walk as a free function so the KAT exercises it on a
// hand-built graph. Consensus-neutral: pure graph traversal, no value semantics
// changed.

#include <cstdint>
#include <functional>
#include <utility>
#include <vector>

namespace dgb {

// Graph accessors over the sharechain, supplied by the caller (production wires
// these to chain.get_index/get_share/contains/get_reverse; the KAT wires them to
// hand-built maps). All are pure reads.
template <typename Hash>
struct P5ChainAccessors {
// naughty count of a share (0 if non-naughty OR absent — mirrors the inline
// `best_idx && best_idx->naughty > 0` guard, where a null index reads as 0).
std::function<int32_t(const Hash&)> naughty;
// m_prev_hash of a share (a null/sentinel Hash if none).
std::function<Hash(const Hash&)> prev_of;
// whether the chain contains a hash.
std::function<bool(const Hash&)> contains;
// reverse-map children of a share (empty if leaf/absent).
std::function<std::vector<Hash>(const Hash&)> children;
// whether a Hash is the null/sentinel value.
std::function<bool(const Hash&)> is_null;
};

template <typename Hash>
struct P5Result {
Hash best; // the share to build on
int32_t punish_val; // self.punish reported out of Phase 5
};

// Deepest non-naughty descendant from `h`, bounded to `limit` generations,
// skipping naughty children. Returns {generations_below_h, deepest_hash}.
// Byte-faithful to the inline `best_desc` lambda (first-strictly-greater wins;
// a node with no eligible kids returns {0, h}).
template <typename Hash>
inline std::pair<int, Hash> think_p5_best_descendant(
const Hash& h, int limit, const P5ChainAccessors<Hash>& acc)
{
if (limit < 0) return {0, h};
auto kids = acc.children(h);
if (kids.empty()) return {0, h};
std::pair<int, Hash> best_kid = {-1, h};
for (const auto& child : kids) {
if (acc.naughty(child) > 0) continue;
auto [gen, hash] = think_p5_best_descendant(child, limit - 1, acc);
if (gen + 1 > best_kid.first) best_kid = {gen + 1, hash};
}
return best_kid.first >= 0 ? best_kid : std::pair<int, Hash>{0, h};
}

// The Phase-5 best-share punish walk. `start` is the highest-scored decorated
// head; `start_valid` is the outer `!best.IsNull() && chain.contains(best)`
// guard — when false, the walk is skipped and {start, 0} is returned verbatim.
template <typename Hash>
inline P5Result<Hash> think_p5_best_share_punish(
const Hash& start, bool start_valid, const P5ChainAccessors<Hash>& acc)
{
Hash best = start;
if (!start_valid)
return {best, 0};

// idx tracks the share best_idx points at; it can diverge from `best` once
// we dive to a descendant (the inline code does NOT re-read best_idx there).
Hash idx = best;
if (acc.naughty(idx) > 0) {
while (acc.naughty(idx) > 0) {
Hash prev = acc.prev_of(best);
if (acc.is_null(prev) || !acc.contains(prev)) break;
best = prev;
idx = prev;
if (acc.naughty(idx) == 0) {
best = think_p5_best_descendant(best, 20, acc).second;
break;
}
}
}
int32_t punish_val = acc.naughty(idx) > 0 ? acc.naughty(idx) : 0;
return {best, punish_val};
}

} // namespace dgb
Loading