From cd431e63da4a7ed3477a6d885d886d9410bbc35b Mon Sep 17 00:00:00 2001 From: frstrtr Date: Wed, 24 Jun 2026 17:14:58 +0000 Subject: [PATCH] dgb: lift naughty-propagation ancestor-punishment into coin SSOT + KAT Phase-B conformance pillar. Lift the pure-integer naughty-generation arithmetic from share_tracker.hpp:561-577 into a header-only SSOT coin/naughty_propagation.hpp and pin it against the p2pool-dgb-scrypt data.py:543-549 oracle with a non-circular KAT. Oracle rule: when a share's previous_share_hash exists and the parent is naughty (>0), child.naughty = 1 + parent.naughty, reset to 0 once it would exceed 6 generations; a non-naughty parent leaves the child's counter untouched. The rule is consensus-bearing -- best_descendent (data.py:812-813) skips naughty children, so it decides which sharechain tails the pool refuses to build on. A silent drift in the +1 step or the 6-generation reset clamp would re-rank tails with no compile error, diverging from the V36 master-compat reference. naughty_child_generation() captures the bare +1/clamp arithmetic; propagate_naughty_from_parent() adds the "parent is naughty" guard and returns std::nullopt when no propagation applies (clean parent). FENCED: header-only, additive, share_tracker.hpp NOT rewired -- the inline body computes the identical value, so the byte-identity delegation is a follow-on. Pure unsigned arithmetic, links only GTest. Target wired into CMakeLists.txt + both build.yml allowlist sites. KAT 4/4 green, expectations hand-derived from the oracle. --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/naughty_propagation.hpp | 75 ++++++++++++++++ src/impl/dgb/test/CMakeLists.txt | 14 +++ .../dgb/test/naughty_propagation_test.cpp | 86 +++++++++++++++++++ 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/coin/naughty_propagation.hpp create mode 100644 src/impl/dgb/test/naughty_propagation_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9fee24f8c..11527ce40 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 nmc_mempool_name_test nmc_block_broadcast_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 dgb_aux_doge_bind_parsers_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_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_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_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_test dgb_naughty_propagation_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 nmc_mempool_name_test nmc_block_broadcast_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 dgb_aux_doge_bind_parsers_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_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_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_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_test dgb_naughty_propagation_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/naughty_propagation.hpp b/src/impl/dgb/coin/naughty_propagation.hpp new file mode 100644 index 000000000..0392df147 --- /dev/null +++ b/src/impl/dgb/coin/naughty_propagation.hpp @@ -0,0 +1,75 @@ +#pragma once + +// SSOT for DGB NAUGHTY-PROPAGATION ancestor punishment -- the pure-integer +// generation arithmetic that taints the descendants of a share which built (or +// would build) an invalid block. A share is marked naughty=1 when it carries an +// excessive block reward (data.py:535-536); thereafter EVERY descendant inherits +// an incrementing generation counter, and that counter is what get_emphemeral +// dispute / best-share selection reads to walk back off a naughty tail +// (data.py:608-611, best_descendent skips naughty children at data.py:812-813). +// +// The propagation rule is consensus-bearing: it decides which shares the pool +// will refuse to build on and which descendants it punishes, so a silent drift +// in the "+1 per generation" step or the "reset after 6 generations" clamp would +// re-rank sharechain tails with NO compile error, diverging operator-facing chain +// selection from the p2pool reference the V36 master-compat invariant pins. +// +// Oracle: p2pool-dgb-scrypt data.py:543-549, evaluated for every share at +// construction after the excessive-reward check: +// +// if self.share_data['previous_share_hash'] and \ +// tracker.items[self.share_data['previous_share_hash']].naughty: +// print "naughty ancestor found %i generations ago" % ... +// self.naughty = 1 + tracker.items[self.share_data['previous_share_hash']].naughty +// if self.naughty > 6: +// self.naughty = 0 +// +// Two observable facts pinned here: +// 1. The guard is "parent EXISTS and parent.naughty is truthy". Python treats +// naughty==0 as falsy, so a non-naughty parent leaves self.naughty UNTOUCHED +// (it keeps whatever the excessive-reward branch set -- 0 normally, 1 if this +// very share is itself a bad-reward share). Propagation NEVER zeroes an +// already-naughty share whose parent happens to be clean. +// 2. When the parent IS naughty (1..6), the child generation is parent+1, and +// a child that would reach generation 7 wraps back to 0 -- i.e. punishment +// spans exactly 6 generations (parent gen 1 -> ... -> gen 6 kept; the would-be +// gen-7 descendant is forgiven). "third and fourth generation" in the oracle +// comment is hyperbole; the code clamps at 6. +// +// Per-coin isolation: dgb/ only. Header-only, additive. This slice does NOT yet +// rewire share_tracker.hpp -- that is the byte-identity delegation follow-on. The +// inline body at share_tracker.hpp:561-577 computes the identical value +// (parent_idx->naughty + 1, then > 6 -> 0), so the follow-on is provably +// value-identical. Pure unsigned arithmetic -> no core link needed. + +#include +#include + +namespace dgb { + +// Child naughty generation given a NAUGHTY parent (parent_naughty >= 1). +// Oracle data.py:547-549: self.naughty = 1 + parent.naughty; if > 6 -> 0. +// Precondition: caller has already established the parent is naughty; this is the +// bare arithmetic so a KAT can pin the +1 step and the 6-generation reset clamp. +inline std::uint32_t naughty_child_generation(std::uint32_t parent_naughty) +{ + std::uint32_t child = 1u + parent_naughty; // data.py:547 + if (child > 6u) // data.py:548 + child = 0u; // data.py:549 -- reset after 6 generations + return child; +} + +// Full propagation INCLUDING the oracle's "parent is naughty" guard +// (data.py:543). Returns the value to assign to the child's naughty counter, or +// std::nullopt when no propagation applies -- i.e. the parent is not naughty, in +// which case the oracle leaves self.naughty untouched (the caller must NOT +// overwrite it). This mirrors the inline guard at share_tracker.hpp:567 +// (`parent_idx->naughty > 0`): assign only inside the if-branch. +inline std::optional propagate_naughty_from_parent(std::uint32_t parent_naughty) +{ + if (parent_naughty == 0u) // data.py:543 -- non-naughty parent is falsy + return std::nullopt; // leave child's naughty as-is + return naughty_child_generation(parent_naughty); +} + +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index e0035a9d5..7cf43ce23 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -766,6 +766,20 @@ if (BUILD_TESTING AND GTest_FOUND) core c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage) gtest_add_tests(dgb_desired_version_tally_test "" AUTO) + # dgb_naughty_propagation_test: FENCED, additive KAT pinning the + # NAUGHTY-PROPAGATION ancestor-punishment generation arithmetic in + # coin/naughty_propagation.hpp vs the p2pool-dgb-scrypt data.py:543-549 + # oracle (child = 1 + parent.naughty; reset to 0 after 6 generations; the + # "parent is naughty" guard leaves a clean-parent child untouched). The rule + # decides which sharechain tails the pool refuses to build on (best_descendent + # skips naughty children), so it is consensus-bearing. Pure unsigned + # arithmetic -> links only GTest. share_tracker.hpp:561-577 NOT yet rewired + # (delegation is the byte-identity follow-on). MUST also be in the build.yml + # --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_naughty_propagation_test naughty_propagation_test.cpp) + target_link_libraries(dgb_naughty_propagation_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_naughty_propagation_test "" AUTO) # dgb_pool_efficiency_test: FENCED, additive KAT pinning the pool # EFFICIENCY / REAL-HASHRATE diagnostics arithmetic in pool_efficiency.hpp vs # the p2pool main.py status-loop oracle (stale_prop = (orphan+doa)/total; diff --git a/src/impl/dgb/test/naughty_propagation_test.cpp b/src/impl/dgb/test/naughty_propagation_test.cpp new file mode 100644 index 000000000..29f77e730 --- /dev/null +++ b/src/impl/dgb/test/naughty_propagation_test.cpp @@ -0,0 +1,86 @@ +// dgb NAUGHTY-PROPAGATION ancestor-punishment KAT. +// +// FENCED, additive (no production code touched this slice). Pins +// src/impl/dgb/coin/naughty_propagation.hpp against the p2pool-dgb-scrypt +// data.py:543-549 propagation rule: +// if prev_share_hash and tracker.items[prev_share_hash].naughty: +// self.naughty = 1 + tracker.items[prev_share_hash].naughty +// if self.naughty > 6: self.naughty = 0 +// +// Every expectation is hand-derived from the oracle Python (the +1-per-generation +// step and the 6-generation reset clamp), NOT read from the code under test, so +// the KAT is non-circular. The inline body at share_tracker.hpp:561-577 is NOT +// yet rewired (delegation is the byte-identity follow-on). Pure unsigned +// arithmetic -> links only GTest. +// MUST appear in BOTH this dir CMakeLists.txt AND the build.yml --target +// allowlist, or it becomes a #143 NOT_BUILT sentinel. + +#include + +#include +#include + +#include + +using dgb::naughty_child_generation; +using dgb::propagate_naughty_from_parent; + +// --- bare child generation = 1 + parent, clamp >6 -> 0 (data.py:547-549) ---- +// Hand-derived table over every reachable parent generation. Parent naughty in +// the live chain is always in [1,6] (it was itself produced by this same rule), +// but we pin 7 too as a defensive over-the-clamp input. +TEST(DgbNaughtyPropagation, ChildGenerationTable) +{ + // parent gen 1 (the bad-reward share itself) -> child gen 2 + EXPECT_EQ(naughty_child_generation(1u), 2u); + EXPECT_EQ(naughty_child_generation(2u), 3u); + EXPECT_EQ(naughty_child_generation(3u), 4u); + EXPECT_EQ(naughty_child_generation(4u), 5u); + // gen 5 -> 6 is the LAST kept generation (6 is not > 6) + EXPECT_EQ(naughty_child_generation(5u), 6u); + // gen 6 -> would-be 7, clamps to 0: the 6th-generation descendant is forgiven + EXPECT_EQ(naughty_child_generation(6u), 0u); + // defensive: an out-of-range parent (7) also wraps to 0 (8 > 6) + EXPECT_EQ(naughty_child_generation(7u), 0u); +} + +// --- the guarded propagation: nullopt when parent not naughty (data.py:543) -- +TEST(DgbNaughtyPropagation, NonNaughtyParentLeavesChildUntouched) +{ + // parent.naughty == 0 is falsy in Python -> no assignment happens. + EXPECT_FALSE(propagate_naughty_from_parent(0u).has_value()); +} + +// --- guarded propagation matches the bare table for naughty parents --------- +TEST(DgbNaughtyPropagation, NaughtyParentPropagates) +{ + EXPECT_EQ(propagate_naughty_from_parent(1u), std::optional(2u)); + EXPECT_EQ(propagate_naughty_from_parent(2u), std::optional(3u)); + EXPECT_EQ(propagate_naughty_from_parent(3u), std::optional(4u)); + EXPECT_EQ(propagate_naughty_from_parent(4u), std::optional(5u)); + EXPECT_EQ(propagate_naughty_from_parent(5u), std::optional(6u)); + // 6th-generation descendant forgiven (assigned 0, NOT left untouched) + EXPECT_EQ(propagate_naughty_from_parent(6u), std::optional(0u)); +} + +// --- non-circular: a full 8-generation chain walk reproduces the oracle ------ +// Seed share is itself naughty (gen 1, excessive reward). Walk forward applying +// the rule and compare against the hand-derived sequence the oracle produces: +// 1 (seed, bad reward), then each child = clamp(1+parent): +// 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 0(forgiven) -> 0(parent 0 = clean, untouched) +TEST(DgbNaughtyPropagation, EightGenerationChainMatchesOracleSequence) +{ + const std::uint32_t expected[8] = {1u, 2u, 3u, 4u, 5u, 6u, 0u, 0u}; + + std::uint32_t gen = 1u; // seed: excessive-reward share, naughty=1 + EXPECT_EQ(gen, expected[0]); + for (int i = 1; i < 8; ++i) { + auto next = propagate_naughty_from_parent(gen); + if (next.has_value()) { + gen = *next; // parent was naughty -> inherit/clamp + } else { + gen = 0u; // parent clean -> child stays at its default 0 + } + EXPECT_EQ(gen, expected[i]) << "generation " << i; + } +}