From 4c2ea55f8ab4b9844dd575e59d3a3c360734c7a4 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sat, 4 Jul 2026 00:31:35 +0000 Subject: [PATCH 1/2] test(dash): S8 stratum-binding contract KAT (extranonce2 nonce64-slot) Fenced test-only slice: pins the get_work() -> stratum job BINDING contract (coinbase nonce64 / extranonce2 slot geometry + coinb1/coinb2 split and reassembly that partitions the miner search space) against oracle frstrtr/p2pool-dash @9a0a609 work.py:21,421,436-438. 6 KATs, all derived independently of the SUT (oracle contract + Python sha256d goldens): Extranonce2WidthOraclePin EXTRANONCE2_SIZE == COINBASE_NONCE_LENGTH == 8 SplitBoundaryGeometry coinb1=bytes[:off], coinb2=bytes[off+8:] ZeroNonceIsIdentity zero nonce reassembles gentx byte-for-byte (:437) SlotIsolation only the 8 slot bytes move across extranonce2 BindingInjectivity distinct extranonce2 -> distinct coinbase leaf GoldenLeafHashes independent sha256d byte-parity anchors Pure/socket-free/node-free (synthetic CoinbaseLayout, no dashd, no sharechain). Touches only test/ + build.yml allowlist -> IDENTIFIER-neutral, non-consensus, independently landable ahead of the migration GO. No self-merge. --- .github/workflows/build.yml | 4 +- test/CMakeLists.txt | 4 + test/test_dash_stratum_binding.cpp | 171 +++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 test/test_dash_stratum_binding.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24b30e7e..da5ef489 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,7 +103,7 @@ jobs: test_threading test_weights \ test_header_chain test_mempool test_template_builder \ test_doge_chain test_compact_blocks test_dash_x11_kat \ - test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split \ + test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split \ test_multiaddress_pplns test_pplns_stress \ test_hash_link test_decay_pplns \ test_pplns_consensus \ @@ -253,7 +253,7 @@ jobs: test_threading test_weights \ test_header_chain test_mempool test_template_builder \ test_doge_chain test_compact_blocks test_dash_x11_kat \ - test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split \ + test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split \ test_hash_link test_decay_pplns \ test_pplns_consensus \ test_v36_script_sorting test_v36_cross_impl_refhash \ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 305d6578..52c120fe 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -320,6 +320,8 @@ if (BUILD_TESTING AND GTest_FOUND) add_executable(test_dash_share_messages test_dash_share_messages.cpp) target_link_libraries(test_dash_share_messages PRIVATE + add_executable(test_dash_stratum_binding test_dash_stratum_binding.cpp) + target_link_libraries(test_dash_stratum_binding PRIVATE GTest::gtest_main GTest::gtest dash_x11 core nlohmann_json::nlohmann_json @@ -327,6 +329,8 @@ if (BUILD_TESTING AND GTest_FOUND) ) target_link_libraries(test_dash_share_messages PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) gtest_add_tests(test_dash_share_messages "" AUTO) + target_link_libraries(test_dash_stratum_binding PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) + gtest_add_tests(test_dash_stratum_binding "" AUTO) # share_init_verify gentx hash_link byte-parity vs oracle (coinbase_payload # VarStr prefix). Header-only over the dash_x11 + core link set; mirrors the diff --git a/test/test_dash_stratum_binding.cpp b/test/test_dash_stratum_binding.cpp new file mode 100644 index 00000000..93ec604c --- /dev/null +++ b/test/test_dash_stratum_binding.cpp @@ -0,0 +1,171 @@ +// DASH S8 STRATUM-BINDING contract KAT. +// +// Pins the get_work() -> stratum job binding contract: the extranonce2 +// (nonce64) coinbase slot geometry and the coinb1/coinb2 split/reassembly +// that partitions the miner search space. Where test_dash_work_job_targets +// pins the job-TARGET arithmetic (work.py:368-426), this pins the coinbase +// NONCE-BINDING half of the same get_work() path (work.py:21,421,436-438). +// +// ORACLE: frstrtr/p2pool-dash @9a0a609 p2pool/work.py +// :21 COINBASE_NONCE_LENGTH = 8 +// :421 coinb1 = packed_gentx[:-payload - COINBASE_NONCE_LENGTH - 4] +// :436 assert len(coinbase_nonce) == COINBASE_NONCE_LENGTH +// :437 new_gentx = coinb1 + coinbase_nonce + packed_gentx[-payload-4:] +// ... == packed_gentx iff coinbase_nonce == '\0'*8 +// Every expected value below is derived from the oracle contract (or an +// independent Python sha256d, see each golden), NOT from the SUT: a true +// byte-parity pin, not a tautology. +// +// Pure / socket-free / node-free: no VM200/201 dashd, no live sharechain, +// no template build -- the contract is exercised on a synthetic CoinbaseLayout +// so the geometry/binding invariants stand alone from generate_transaction. + +#include + +#include +#include +#include +#include +#include + +#include // CoinbaseLayout, split_coinb, sha256d, EXTRANONCE2_SIZE +#include // HexStr +#include + +using dash::coinbase::CoinbaseLayout; +using dash::coinbase::CoinbSplit; +using dash::coinbase::split_coinb; +using dash::coinbase::sha256d; +using dash::coinbase::EXTRANONCE2_SIZE; + +namespace { + +// Synthetic 40-byte coinbase: bytes[i] = i, with the 8-byte nonce64 slot at +// [28,36) zeroed (nonce64_offset = 40 - locktime(4) - nonce64(8) = 28), mirroring +// the real coinbase tail [... nonce64 8B][locktime 4B] for a type-0 tx. +constexpr size_t kTotal = 40; +constexpr size_t kOffset = 28; // = kTotal - 4 - 8 + +CoinbaseLayout make_layout() +{ + CoinbaseLayout lay; + lay.bytes.resize(kTotal); + std::iota(lay.bytes.begin(), lay.bytes.end(), static_cast(0)); + for (size_t i = kOffset; i < kOffset + EXTRANONCE2_SIZE; ++i) + lay.bytes[i] = 0x00; + lay.nonce64_offset = kOffset; + lay.ref_hash_offset = kOffset - 32; // not exercised here; kept consistent + return lay; +} + +// Reassemble coinb1 || extranonce2 || coinb2 as a stratum miner does, then +// sha256d it -- the coinbase-hash leaf the miner feeds the merkle root. +std::vector reassemble(const CoinbaseLayout& lay, + std::span e2) +{ + std::vector cb; + cb.reserve(lay.bytes.size()); + cb.insert(cb.end(), lay.bytes.begin(), lay.bytes.begin() + lay.nonce64_offset); + cb.insert(cb.end(), e2.begin(), e2.end()); + cb.insert(cb.end(), lay.bytes.begin() + lay.nonce64_offset + EXTRANONCE2_SIZE, + lay.bytes.end()); + return cb; +} + +uint256 hash_of(const std::vector& b) +{ + return sha256d(std::span(b.data(), b.size())); +} + +const unsigned char E2_A[8] = {1, 2, 3, 4, 5, 6, 7, 8}; +const unsigned char E2_B[8] = {0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xf0, 0x0d}; +const unsigned char E2_ZERO[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + +} // namespace + +// (1) Extranonce2 width == oracle COINBASE_NONCE_LENGTH (work.py:21). A width +// mismatch silently corrupts every miner submission, so pin it hard. +TEST(DashStratumBinding, Extranonce2WidthOraclePin) +{ + EXPECT_EQ(EXTRANONCE2_SIZE, static_cast(8)); +} + +// (2) Split boundary geometry (work.py:421). coinb1 is exactly bytes[:offset] +// and coinb2 is exactly bytes[offset+8:]. Lengths pinned from the layout, +// independent of the SUT's own hex. +TEST(DashStratumBinding, SplitBoundaryGeometry) +{ + CoinbaseLayout lay = make_layout(); + CoinbSplit s = split_coinb(lay); + EXPECT_EQ(s.coinb1_hex.size(), kOffset * 2); // hex chars + EXPECT_EQ(s.coinb2_hex.size(), (kTotal - kOffset - EXTRANONCE2_SIZE) * 2); + // coinb1 == HexStr(bytes[:offset]); coinb2 == HexStr(bytes[offset+8:]). + std::span b1(lay.bytes.data(), kOffset); + std::span b2(lay.bytes.data() + kOffset + EXTRANONCE2_SIZE, + kTotal - kOffset - EXTRANONCE2_SIZE); + EXPECT_EQ(s.coinb1_hex, HexStr(b1)); + EXPECT_EQ(s.coinb2_hex, HexStr(b2)); +} + +// (3) Identity: reassembling coinb1 || '\0'*8 || coinb2 reproduces the coinbase +// bytes EXACTLY (work.py:437 -- coinbase_nonce == '\0'*8 => new_gentx == gentx). +// The zero nonce is the identity element of the binding. +TEST(DashStratumBinding, ZeroNonceIsIdentity) +{ + CoinbaseLayout lay = make_layout(); + CoinbSplit s = split_coinb(lay); + std::span z(E2_ZERO, EXTRANONCE2_SIZE); + EXPECT_EQ(s.coinb1_hex + HexStr(z) + s.coinb2_hex, HexStr(lay.bytes)); + // ...and the reassembled bytes equal the original coinbase byte-for-byte. + EXPECT_EQ(reassemble(lay, z), lay.bytes); +} + +// (4) Slot isolation: across distinct extranonce2 values, coinb1 and coinb2 are +// invariant and ONLY the 8 slot bytes move -- the reason a miner fetches +// coinb1/coinb2 once per job and iterates just the slot. +TEST(DashStratumBinding, SlotIsolation) +{ + CoinbaseLayout lay = make_layout(); + auto a = reassemble(lay, std::span(E2_A, 8)); + auto b = reassemble(lay, std::span(E2_B, 8)); + ASSERT_EQ(a.size(), lay.bytes.size()); + ASSERT_EQ(b.size(), lay.bytes.size()); + for (size_t i = 0; i < a.size(); ++i) { + if (i >= kOffset && i < kOffset + EXTRANONCE2_SIZE) continue; // slot: may differ + EXPECT_EQ(a[i], b[i]) << "byte " << i << " outside slot must be invariant"; + } + // slot region carries the supplied extranonce2 verbatim. + for (size_t i = 0; i < EXTRANONCE2_SIZE; ++i) { + EXPECT_EQ(a[kOffset + i], E2_A[i]); + EXPECT_EQ(b[kOffset + i], E2_B[i]); + } +} + +// (5) Binding injectivity: distinct extranonce2 -> distinct coinbase hash, and +// each differs from the zero-slot leaf. This is the search-space PARTITION +// guarantee -- overlapping leaves would let two miners claim one solution. +TEST(DashStratumBinding, BindingInjectivity) +{ + CoinbaseLayout lay = make_layout(); + uint256 h0 = hash_of(reassemble(lay, std::span(E2_ZERO, 8))); + uint256 hA = hash_of(reassemble(lay, std::span(E2_A, 8))); + uint256 hB = hash_of(reassemble(lay, std::span(E2_B, 8))); + EXPECT_NE(hA, hB); + EXPECT_NE(hA, h0); + EXPECT_NE(hB, h0); +} + +// (6) Golden sha256d byte-parity anchors. Expected values computed independently +// in Python (hashlib sha256d, big-endian display), NOT from the SUT: +// base = bytes(range(40)); base[28:36] = 0 +// zero/A/B insert '\0'*8 / 01..08 / deadbeefcafef00d at [28:36) +TEST(DashStratumBinding, GoldenLeafHashes) +{ + CoinbaseLayout lay = make_layout(); + EXPECT_EQ(hash_of(reassemble(lay, std::span(E2_ZERO, 8))).GetHex(), + "b0d22de8e7f6765f9d8c289ecd77ad8e0ab6a88c2a4ccf594c509d7775bb54ab"); + EXPECT_EQ(hash_of(reassemble(lay, std::span(E2_A, 8))).GetHex(), + "9bdc1a3b7b8600535e99c5298b6cb50fde48bd1b20a1ad6883abfbb2799b1b0e"); + EXPECT_EQ(hash_of(reassemble(lay, std::span(E2_B, 8))).GetHex(), + "a0acea5d125dd3452a24dd84c0d1245450dd237cfe0bc0170d4381195e5f8061"); +} From 1e6f4cbe74bf088fa033a888f495ef81b9f6e36f Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sat, 4 Jul 2026 00:49:18 +0000 Subject: [PATCH 2/2] fix(dash): untangle test/CMakeLists.txt stratum-binding merge resolution Rebase-clean union of the #628-vs-#630 add_executable collision left target_link_libraries(test_dash_share_messages) opened but unterminated with add_executable(test_dash_stratum_binding) interleaved into it, producing CMake parse error at test/CMakeLists.txt:322 (missing ending ) and breaking configure on all platforms (macOS/Windows failed fast; Linux would follow). Split into two complete, separate target blocks. Configure clean; test_dash_stratum_binding builds; ctest 6/6. --- test/CMakeLists.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 52c120fe..9cfd8734 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -320,8 +320,6 @@ if (BUILD_TESTING AND GTest_FOUND) add_executable(test_dash_share_messages test_dash_share_messages.cpp) target_link_libraries(test_dash_share_messages PRIVATE - add_executable(test_dash_stratum_binding test_dash_stratum_binding.cpp) - target_link_libraries(test_dash_stratum_binding PRIVATE GTest::gtest_main GTest::gtest dash_x11 core nlohmann_json::nlohmann_json @@ -329,6 +327,14 @@ if (BUILD_TESTING AND GTest_FOUND) ) target_link_libraries(test_dash_share_messages PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) gtest_add_tests(test_dash_share_messages "" AUTO) + + add_executable(test_dash_stratum_binding test_dash_stratum_binding.cpp) + target_link_libraries(test_dash_stratum_binding PRIVATE + GTest::gtest_main GTest::gtest + dash_x11 core + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) target_link_libraries(test_dash_stratum_binding PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) gtest_add_tests(test_dash_stratum_binding "" AUTO)