From 3109eb60a860f08f3f3a47218e03280765774168 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 21 Jun 2026 11:56:35 +0000 Subject: [PATCH] dgb: Stage 4d mining_submit decision SSOT (submit_classify.hpp) Add coin/submit_classify.hpp: the single place mining_submit places a submission Scrypt pow_hash into one of three outcome classes. Tighten-first ladder (block target checked before share target, so a won block -- a superset of an accepted share -- wins the classification): pow_hash <= block_target -> WonBlock (submit_block_fn_ broadcast) pow_hash <= share_target -> ShareAccept (try_mint_share sharechain mint) otherwise -> Reject Inclusive at BOTH boundaries via the same pow<=target == !(pow>target) gate the header_chain satisfaction check and the nonce grinder run, matching DigiByte Core CheckProofOfWork and p2pool-merged-v36 share-accept -- keeps c2pool-dgb bit-compatible with the daemon and the Python reference. Header-only, depends only on dgb_arith256.hpp (u256): no scrypt, no core, no dgb OBJECT lib -- a pure decision over three integers. 7/7 KAT pins the ladder, both inclusive boundaries, the zero-hash superset case, and a safety invariant that an inverted (malformed) target pair can only mis-Reject, never spurious WonBlock. Registered in both build.yml --target allowlists (#143 trap). mining_submit still returns the stage-4d low-difficulty stub -- no behavior change. The header-reconstruct -> scrypt_pow_hash -> classify -> dispatch wiring that makes this the live-invocation point is the following slice. --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/submit_classify.hpp | 64 ++++++++++++++++++ src/impl/dgb/test/CMakeLists.txt | 13 ++++ src/impl/dgb/test/submit_classify_test.cpp | 78 ++++++++++++++++++++++ 4 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/coin/submit_classify.hpp create mode 100644 src/impl/dgb/test/submit_classify_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 881499708..cf5e3f8c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,7 +82,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_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_coinbase_value_parity_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_coinbase_value_parity_test dgb_submit_classify_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 v37_test \ -j$(nproc) @@ -214,7 +214,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_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_coinbase_value_parity_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_coinbase_value_parity_test dgb_submit_classify_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 test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ diff --git a/src/impl/dgb/coin/submit_classify.hpp b/src/impl/dgb/coin/submit_classify.hpp new file mode 100644 index 000000000..75cdd1556 --- /dev/null +++ b/src/impl/dgb/coin/submit_classify.hpp @@ -0,0 +1,64 @@ +#pragma once +// --------------------------------------------------------------------------- +// submit_classify.hpp -- the Stage 4d mining_submit decision SSOT. +// +// mining_submit (stratum/work_source.cpp) reconstructs the 80-byte block header +// from the miner submission, runs scrypt_pow_hash (#286 SSOT) to obtain the +// DGB-Scrypt pow_hash u256, then must place the result in EXACTLY ONE of three +// outcome classes. THIS header is the single place that three-way decision is +// made, so the hot path and its KATs can never disagree on the boundary +// semantics, and the eventual dual-path broadcaster + sharechain-mint dispatch +// both read their trigger from one function. +// +// The ladder, in strict tighten-first order. The block target is always <= the +// share target, so a hash that meets the block target necessarily meets the +// share target: a won block is a SUPERSET of an accepted share, never a +// disjoint class. Check the block target FIRST so the rarer, higher-value +// outcome wins the classification: +// +// pow_hash <= block_target -> WonBlock (submit_block_fn_: broadcast) +// pow_hash <= share_target -> ShareAccept (try_mint_share: sharechain mint) +// otherwise -> Reject (low-difficulty stratum error) +// +// The comparison is the EXACT inclusive gate the header_chain satisfaction +// check and the nonce grinder run: pow <= target == !(pow > target), MSB-first +// via u256::operator>. Inclusive at BOTH boundaries -- a hash exactly equal to +// a target satisfies it. This matches Bitcoin / DigiByte Core CheckProofOfWork +// (hash > target is the ONLY reject) and p2pool s share-accept gate, so +// c2pool-dgb stays bit-compatible with the daemon AND with p2pool-merged-v36. +// +// Header-only, depends ONLY on dgb_arith256.hpp (u256): no core, no daemon, no +// scrypt (the caller supplies the already-computed digest). Same no-link +// discipline as scrypt_pow.hpp / nonce_grinder.hpp -- a pure decision over +// three integers, trivially testable without the stratum string machinery. +// --------------------------------------------------------------------------- + +#include // dgb::coin::u256 + +namespace dgb::coin { + +// The three mutually-exclusive outcomes of a stratum submission, in increasing +// difficulty order. Values are stable (logged / asserted by the KATs). +enum class SubmitClass { + Reject = 0, // pow_hash > share_target: below share difficulty + ShareAccept = 1, // share_target >= pow_hash > block_target: mint a share + WonBlock = 2, // pow_hash <= block_target: a full network block was found +}; + +// Classify a submission s Scrypt pow_hash against the two targets. +// +// Precondition (caller-guaranteed): block_target <= share_target -- the share +// target is always looser. The tighten-first ladder is SAFE even if a malformed +// job inverts them: a hash is only ever called WonBlock when it genuinely meets +// the block target, so an inverted pair can never promote a share into a +// spurious block broadcast (it would at worst mis-Reject, never mis-WonBlock). +inline SubmitClass classify_submission(const u256& pow_hash, + const u256& block_target, + const u256& share_target) +{ + if (!(pow_hash > block_target)) return SubmitClass::WonBlock; // <= block + if (!(pow_hash > share_target)) return SubmitClass::ShareAccept; // <= share + return SubmitClass::Reject; +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index ddf1ae443..af10c1792 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -406,4 +406,17 @@ if (BUILD_TESTING AND GTest_FOUND) GTest::gtest_main GTest::gtest) gtest_add_tests(dgb_won_block_finalize_test "" AUTO) + # --- #82 Stage 4d: mining_submit decision SSOT (coin/submit_classify.hpp) -- + # Pins classify_submission: the three-way pow_hash ladder (WonBlock / + # ShareAccept / Reject) with BOTH boundaries inclusive, mirroring DigiByte + # Core CheckProofOfWork (hash>target = the only reject) and p2pool-merged-v36 + # share-accept. Tighten-first order so block beats share; safety KAT proves an + # inverted (malformed) target pair can only mis-Reject, never spurious-WonBlock. + # Pure header-only u256 -- no scrypt, no core, no dgb OBJECT lib. MUST stay in + # BOTH build.yml --target allowlists (#143 trap). + add_executable(dgb_submit_classify_test submit_classify_test.cpp) + target_link_libraries(dgb_submit_classify_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_submit_classify_test "" AUTO) + endif() diff --git a/src/impl/dgb/test/submit_classify_test.cpp b/src/impl/dgb/test/submit_classify_test.cpp new file mode 100644 index 000000000..54115c5f7 --- /dev/null +++ b/src/impl/dgb/test/submit_classify_test.cpp @@ -0,0 +1,78 @@ +// --------------------------------------------------------------------------- +// submit_classify_test.cpp -- KAT for the Stage 4d mining_submit decision SSOT +// (coin/submit_classify.hpp). Pins the three-way ladder + BOTH inclusive +// boundaries so the hot path (work_source.cpp mining_submit) and the dual-path +// broadcaster / sharechain-mint dispatch can never drift from the documented +// pow<=target gate, which itself mirrors DigiByte Core CheckProofOfWork and +// p2pool-merged-v36 share-accept. Pure u256 arithmetic -- no scrypt, no link. +// --------------------------------------------------------------------------- +#include + +#include + +using dgb::coin::u256; +using dgb::coin::SubmitClass; +using dgb::coin::classify_submission; + +namespace { + +// Realistic ordering: block target is TIGHTER (smaller) than the share target. +// Use round magnitudes so the boundaries are unambiguous. +// block_target = 1000, share_target = 1000000. +u256 block_t() { return u256::from_u64(1000); } +u256 share_t() { return u256::from_u64(1000000); } + +u256 plus1(u256 v) { v += u256::from_u64(1); return v; } + +} // namespace + +// A hash STRICTLY below the block target is a won block (rarer outcome wins). +TEST(DgbSubmitClassify, BelowBlockTargetIsWonBlock) { + EXPECT_EQ(classify_submission(u256::from_u64(500), block_t(), share_t()), + SubmitClass::WonBlock); +} + +// Inclusive at the BLOCK boundary: pow == block_target satisfies it (won). +TEST(DgbSubmitClassify, EqualBlockTargetIsWonBlockInclusive) { + EXPECT_EQ(classify_submission(block_t(), block_t(), share_t()), + SubmitClass::WonBlock); +} + +// One unit above the block target, still under the share target -> share mint. +TEST(DgbSubmitClassify, JustAboveBlockTargetIsShareAccept) { + EXPECT_EQ(classify_submission(plus1(block_t()), block_t(), share_t()), + SubmitClass::ShareAccept); +} + +// Inclusive at the SHARE boundary: pow == share_target is still an accept. +TEST(DgbSubmitClassify, EqualShareTargetIsShareAcceptInclusive) { + EXPECT_EQ(classify_submission(share_t(), block_t(), share_t()), + SubmitClass::ShareAccept); +} + +// One unit above the share target -> below share difficulty -> reject. +TEST(DgbSubmitClassify, JustAboveShareTargetIsReject) { + EXPECT_EQ(classify_submission(plus1(share_t()), block_t(), share_t()), + SubmitClass::Reject); +} + +// pow_hash == 0 trivially satisfies every target -> won block (superset rule: +// a won block always also clears the share target; block is checked first). +TEST(DgbSubmitClassify, ZeroHashIsWonBlockNotDoubleCounted) { + EXPECT_EQ(classify_submission(u256::from_u64(0), block_t(), share_t()), + SubmitClass::WonBlock); +} + +// Safety under an inverted (malformed) job where share_target < block_target: +// the tighten-first ladder must NEVER promote a non-block hash to WonBlock. +// Here pow=2000 is below the (looser-looking) block_t=1000? no -- it is ABOVE +// 1000, so it is not a block; with share=500 it is also above 500 -> Reject. +// The invariant under test: an inverted pair can only mis-Reject, never +// mis-WonBlock. +TEST(DgbSubmitClassify, InvertedTargetsNeverSpuriousWonBlock) { + const u256 tight = u256::from_u64(500); // passed as share (wrong) + const u256 loose = u256::from_u64(1000); // passed as block (wrong) + // pow just above the real block magnitude (1000): not a block. + EXPECT_NE(classify_submission(u256::from_u64(1500), loose, tight), + SubmitClass::WonBlock); +}