diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da5ef489..70461ab1 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_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_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_job_notify_roundtrip 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_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_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_job_notify_roundtrip 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 9cfd8734..154ff40d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -317,6 +317,15 @@ if (BUILD_TESTING AND GTest_FOUND) ) target_link_libraries(test_dash_work_job_targets PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39): core stratum/web_server TUs gtest_add_tests(test_dash_work_job_targets "" AUTO) + add_executable(test_dash_job_notify_roundtrip test_dash_job_notify_roundtrip.cpp) + target_link_libraries(test_dash_job_notify_roundtrip PRIVATE + GTest::gtest_main GTest::gtest + dash_x11 core + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + target_link_libraries(test_dash_job_notify_roundtrip PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) + gtest_add_tests(test_dash_job_notify_roundtrip "" AUTO) add_executable(test_dash_share_messages test_dash_share_messages.cpp) target_link_libraries(test_dash_share_messages PRIVATE diff --git a/test/test_dash_job_notify_roundtrip.cpp b/test/test_dash_job_notify_roundtrip.cpp new file mode 100644 index 00000000..d6738681 --- /dev/null +++ b/test/test_dash_job_notify_roundtrip.cpp @@ -0,0 +1,215 @@ +// DASH S8 STRATUM-BINDING contract KAT -- job-notify ROUND-TRIP leaf. +// +// The final leaf of the S8 stratum-binding suite. Where test_dash_stratum_binding +// pins the coinbase NONCE slot geometry (the get_work()->job half) and +// test_dash_work_job_targets pins the job TARGET arithmetic, THIS pins the +// notify->submit seam: the merkle_branch the pool sends ONCE per job must fold +// the miner's reassembled coinbase back to the SAME canonical merkle root the +// pool computes over the full tx list. That equality is the whole reason a +// miner can iterate extranonce2 locally without re-fetching the job -- if the +// branch did not bind, every submit would carry a root the pool rejects. +// +// ORACLE: frstrtr/p2pool-dash @9a0a609 +// dash/stratum.py -- mining.notify packs merkle branch as +// [pack.IntType(256).pack(s).encode('hex') for s in link.branch] +// i.e. LE internal bytes as hex (NOT the reversed display form). +// p2pool/work.py -- coinb1/coinb2 split at the nonce64 slot (:421,:436-437). +// Every expected value below is derived from an INDEPENDENT Python sha256d +// merkle walk (see the golden block), NOT from the SUT: a true byte-parity pin. +// +// Pure / socket-free / node-free: synthetic coinbase + synthetic sibling tx +// hashes, no dashd, no live sharechain, no template build. The round-trip +// invariant stands alone from generate_transaction / the stratum server. + +#include + +#include +#include +#include +#include +#include +#include + +#include // split_coinb, merkle_branches_raw/hex, 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::merkle_branches_raw; +using dash::coinbase::merkle_branches_hex; +using dash::coinbase::EXTRANONCE2_SIZE; + +namespace { + +// Synthetic 40-byte coinbase identical to test_dash_stratum_binding: bytes[i]=i +// with the 8-byte nonce64 slot at [28,36) zeroed. (Leaf0 goldens below MATCH +// that sibling test, confirming a shared layout.) +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; + return lay; +} + +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())); +} + +// Synthetic sibling tx hashes (indices 1..3 of the merkle tree). Values are +// arbitrary but fixed; the Python oracle uses the same sha256d(bytes([v]*4)). +uint256 mk_tx(unsigned char v) +{ + std::vector b(4, v); + return sha256d(std::span(b.data(), b.size())); +} + +// Pool-side canonical merkle root over [coinbase_leaf, h1, h2, h3] -- computed +// in-test, independent of merkle_branches_raw, so the round-trip check is not a +// tautology against the SUT branch. +uint256 canonical_root(uint256 leaf0, uint256 h1, uint256 h2, uint256 h3) +{ + std::vector layer{leaf0, h1, h2, h3}; + while (layer.size() > 1) { + if (layer.size() % 2 == 1) layer.push_back(layer.back()); + std::vector next; + for (size_t i = 0; i + 1 < layer.size(); i += 2) { + std::vector buf(64); + std::memcpy(buf.data(), layer[i].data(), 32); + std::memcpy(buf.data() + 32, layer[i + 1].data(), 32); + next.push_back(sha256d(std::span(buf.data(), buf.size()))); + } + layer.swap(next); + } + return layer[0]; +} + +// Miner-side walk: fold the coinbase leaf through the branch siblings, exactly +// as cpuminer does with the mining.notify merkle_branch. root = leaf; for each +// sibling s: root = sha256d(root || s). +uint256 miner_root(uint256 leaf0, const std::vector& branch) +{ + uint256 r = leaf0; + for (const auto& s : branch) { + std::vector buf(64); + std::memcpy(buf.data(), r.data(), 32); + std::memcpy(buf.data() + 32, s.data(), 32); + r = sha256d(std::span(buf.data(), buf.size())); + } + return r; +} + +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}; + +// The branch the pool computes ONCE per job, from the placeholder coinbase +// (slot 0 == zero-hash) plus the three sibling tx hashes. Siblings are +// independent of leaf 0, which is exactly what makes the branch reusable. +std::vector job_branch() +{ + std::vector leaves{uint256::ZERO, mk_tx(0xa1), mk_tx(0xb2), mk_tx(0xc3)}; + return merkle_branches_raw(leaves); +} + +} // namespace + +// (1) The notify branch has the expected depth for a 4-leaf tree: two siblings +// (one per merkle level). A wrong-depth branch silently mis-binds. +TEST(DashJobNotifyRoundTrip, BranchDepth) +{ + EXPECT_EQ(job_branch().size(), static_cast(2)); +} + +// (2) ROUND-TRIP core: for the zero extranonce2 (identity), the miner walk over +// the notify branch reproduces the pool canonical root byte-for-byte. +TEST(DashJobNotifyRoundTrip, ZeroNonceRootMatches) +{ + CoinbaseLayout lay = make_layout(); + uint256 h1 = mk_tx(0xa1), h2 = mk_tx(0xb2), h3 = mk_tx(0xc3); + uint256 leaf0 = hash_of(reassemble(lay, std::span(E2_ZERO, 8))); + EXPECT_EQ(miner_root(leaf0, job_branch()), canonical_root(leaf0, h1, h2, h3)); +} + +// (3) ROUND-TRIP under extranonce2 iteration: the SAME job branch binds every +// distinct extranonce2 back to the matching canonical root -- the miner +// iterates the slot without re-fetching the job. +TEST(DashJobNotifyRoundTrip, IteratedNonceRootMatches) +{ + CoinbaseLayout lay = make_layout(); + uint256 h1 = mk_tx(0xa1), h2 = mk_tx(0xb2), h3 = mk_tx(0xc3); + const std::vector br = job_branch(); + for (const auto* e2 : {E2_A, E2_B}) { + uint256 leaf0 = hash_of(reassemble(lay, std::span(e2, 8))); + EXPECT_EQ(miner_root(leaf0, br), canonical_root(leaf0, h1, h2, h3)); + } +} + +// (4) Distinct extranonce2 -> distinct block merkle root: the notify branch +// PARTITIONS the search space rather than collapsing it (overlapping roots +// would let two miners claim one solution). +TEST(DashJobNotifyRoundTrip, RootInjectivity) +{ + CoinbaseLayout lay = make_layout(); + const std::vector br = job_branch(); + uint256 r0 = miner_root(hash_of(reassemble(lay, std::span(E2_ZERO, 8))), br); + uint256 rA = miner_root(hash_of(reassemble(lay, std::span(E2_A, 8))), br); + uint256 rB = miner_root(hash_of(reassemble(lay, std::span(E2_B, 8))), br); + EXPECT_NE(rA, rB); + EXPECT_NE(rA, r0); + EXPECT_NE(rB, r0); +} + +// (5) Wire form of the merkle branch is LE internal-bytes hex (stratum.py +// convention), NOT the reversed display form. Golden hex from independent +// Python (pack.IntType(256).pack(s).encode(hex)). +TEST(DashJobNotifyRoundTrip, BranchWireHexGolden) +{ + std::vector hex = merkle_branches_hex(job_branch()); + ASSERT_EQ(hex.size(), static_cast(2)); + EXPECT_EQ(hex[0], "f8401cfffc19b3a56c2de52278297152d9028b4efbc30f8d2096f780ceb0b2b6"); + EXPECT_EQ(hex[1], "6f43268501a527c6adc37e7d6a293168c42f9aae863ab15628ab78ddf9ec836c"); +} + +// (6) Golden merkle-root byte-parity anchors (GetHex display form), computed +// independently in Python over [sha256d(coinbase_e2), h1, h2, h3]: +// h_i = sha256d(bytes([0xa1/0xb2/0xc3]*4)) +// leaf0 = sha256d(base[:28] + e2 + base[36:]), base = bytes(range(40)) w/ [28:36]=0 +TEST(DashJobNotifyRoundTrip, CanonicalRootGolden) +{ + CoinbaseLayout lay = make_layout(); + uint256 h1 = mk_tx(0xa1), h2 = mk_tx(0xb2), h3 = mk_tx(0xc3); + auto root_for = [&](const unsigned char* e2) { + return canonical_root( + hash_of(reassemble(lay, std::span(e2, 8))), h1, h2, h3); + }; + EXPECT_EQ(root_for(E2_ZERO).GetHex(), + "b4bfbae433a5f984c30f4bead3cfe650a0e770bd8126b5c16a15b4b5b9243189"); + EXPECT_EQ(root_for(E2_A).GetHex(), + "d445471556d06a5b2722c27f92498bf030117d756fd8fe7997d3732cf4df5e93"); + EXPECT_EQ(root_for(E2_B).GetHex(), + "7c81c0a061417d008d0ada06bfe98d69d82ea6561ec1c327f7fb34d63259772c"); +}