diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9edc16849..e9490e4e3 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 \ 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 @@ -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) diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index c02b310b7..fe0c59b76 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -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() diff --git a/src/impl/dgb/test/think_p5_best_share_punish_test.cpp b/src/impl/dgb/test/think_p5_best_share_punish_test.cpp new file mode 100644 index 000000000..c4b8a8695 --- /dev/null +++ b/src/impl/dgb/test/think_p5_best_share_punish_test.cpp @@ -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 + +#include + +#include +#include +#include + +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 naughty; // present => contained + std::map prev; // hash -> prev hash ("" = none) + + dgb::P5ChainAccessors accessors() const { + dgb::P5ChainAccessors 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::vector 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("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("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("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("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("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("P", true, c.accessors()); + EXPECT_EQ(r.best, "P"); + EXPECT_EQ(r.punish_val, 2); +} + +} // namespace diff --git a/src/impl/dgb/think_p5_best_share_punish.hpp b/src/impl/dgb/think_p5_best_share_punish.hpp new file mode 100644 index 000000000..4e7dc0dc3 --- /dev/null +++ b/src/impl/dgb/think_p5_best_share_punish.hpp @@ -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 +#include +#include +#include + +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 +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 naughty; + // m_prev_hash of a share (a null/sentinel Hash if none). + std::function prev_of; + // whether the chain contains a hash. + std::function contains; + // reverse-map children of a share (empty if leaf/absent). + std::function(const Hash&)> children; + // whether a Hash is the null/sentinel value. + std::function is_null; +}; + +template +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 +inline std::pair think_p5_best_descendant( + const Hash& h, int limit, const P5ChainAccessors& acc) +{ + if (limit < 0) return {0, h}; + auto kids = acc.children(h); + if (kids.empty()) return {0, h}; + std::pair 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{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 +inline P5Result think_p5_best_share_punish( + const Hash& start, bool start_valid, const P5ChainAccessors& 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