Skip to content

Commit 2d403f2

Browse files
committed
test(dash): S8 job-notify round-trip KAT — merkle branch binds reassembled coinbase to canonical root
Final leaf of the S8 stratum-binding suite. Pins the notify->submit seam: the mining.notify merkle_branch (sent once per job) must fold the miners reassembled coinb1||extranonce2||coinb2 back to the SAME canonical merkle root the pool computes over the full tx list, for every extranonce2. That equality is why a miner iterates the slot locally without re-fetching. Oracle: p2pool-dash @9a0a609 dash/stratum.py (LE merkle-branch packing) + work.py:421,436-437 (coinb1/coinb2 split). All goldens from an independent Python sha256d walk; leaf0 anchors match test_dash_stratum_binding. Pure / socket-free / node-free synthetic layout; DASH-tree-fenced (test/ + CMakeLists + build.yml allowlist only), non-consensus.
1 parent 1bdef6b commit 2d403f2

3 files changed

Lines changed: 226 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ jobs:
103103
test_threading test_weights \
104104
test_header_chain test_mempool test_template_builder \
105105
test_doge_chain test_compact_blocks test_dash_x11_kat \
106-
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 \
106+
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_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 \
107107
test_multiaddress_pplns test_pplns_stress \
108108
test_hash_link test_decay_pplns \
109109
test_pplns_consensus \
@@ -253,7 +253,7 @@ jobs:
253253
test_threading test_weights \
254254
test_header_chain test_mempool test_template_builder \
255255
test_doge_chain test_compact_blocks test_dash_x11_kat \
256-
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 \
256+
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_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 \
257257
test_hash_link test_decay_pplns \
258258
test_pplns_consensus \
259259
test_v36_script_sorting test_v36_cross_impl_refhash \

test/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,15 @@ if (BUILD_TESTING AND GTest_FOUND)
317317
)
318318
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
319319
gtest_add_tests(test_dash_work_job_targets "" AUTO)
320+
add_executable(test_dash_job_notify_roundtrip test_dash_job_notify_roundtrip.cpp)
321+
target_link_libraries(test_dash_job_notify_roundtrip PRIVATE
322+
GTest::gtest_main GTest::gtest
323+
dash_x11 core
324+
nlohmann_json::nlohmann_json
325+
${Boost_LIBRARIES}
326+
)
327+
target_link_libraries(test_dash_job_notify_roundtrip PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
328+
gtest_add_tests(test_dash_job_notify_roundtrip "" AUTO)
320329

321330
add_executable(test_dash_share_messages test_dash_share_messages.cpp)
322331
target_link_libraries(test_dash_share_messages PRIVATE
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// DASH S8 STRATUM-BINDING contract KAT -- job-notify ROUND-TRIP leaf.
2+
//
3+
// The final leaf of the S8 stratum-binding suite. Where test_dash_stratum_binding
4+
// pins the coinbase NONCE slot geometry (the get_work()->job half) and
5+
// test_dash_work_job_targets pins the job TARGET arithmetic, THIS pins the
6+
// notify->submit seam: the merkle_branch the pool sends ONCE per job must fold
7+
// the miner's reassembled coinbase back to the SAME canonical merkle root the
8+
// pool computes over the full tx list. That equality is the whole reason a
9+
// miner can iterate extranonce2 locally without re-fetching the job -- if the
10+
// branch did not bind, every submit would carry a root the pool rejects.
11+
//
12+
// ORACLE: frstrtr/p2pool-dash @9a0a609
13+
// dash/stratum.py -- mining.notify packs merkle branch as
14+
// [pack.IntType(256).pack(s).encode('hex') for s in link.branch]
15+
// i.e. LE internal bytes as hex (NOT the reversed display form).
16+
// p2pool/work.py -- coinb1/coinb2 split at the nonce64 slot (:421,:436-437).
17+
// Every expected value below is derived from an INDEPENDENT Python sha256d
18+
// merkle walk (see the golden block), NOT from the SUT: a true byte-parity pin.
19+
//
20+
// Pure / socket-free / node-free: synthetic coinbase + synthetic sibling tx
21+
// hashes, no dashd, no live sharechain, no template build. The round-trip
22+
// invariant stands alone from generate_transaction / the stratum server.
23+
24+
#include <gtest/gtest.h>
25+
26+
#include <cstddef>
27+
#include <cstring>
28+
#include <numeric>
29+
#include <span>
30+
#include <string>
31+
#include <vector>
32+
33+
#include <impl/dash/coinbase_builder.hpp> // split_coinb, merkle_branches_raw/hex, sha256d, EXTRANONCE2_SIZE
34+
#include <btclibs/util/strencodings.h> // HexStr
35+
#include <core/uint256.hpp>
36+
37+
using dash::coinbase::CoinbaseLayout;
38+
using dash::coinbase::CoinbSplit;
39+
using dash::coinbase::split_coinb;
40+
using dash::coinbase::sha256d;
41+
using dash::coinbase::merkle_branches_raw;
42+
using dash::coinbase::merkle_branches_hex;
43+
using dash::coinbase::EXTRANONCE2_SIZE;
44+
45+
namespace {
46+
47+
// Synthetic 40-byte coinbase identical to test_dash_stratum_binding: bytes[i]=i
48+
// with the 8-byte nonce64 slot at [28,36) zeroed. (Leaf0 goldens below MATCH
49+
// that sibling test, confirming a shared layout.)
50+
constexpr size_t kTotal = 40;
51+
constexpr size_t kOffset = 28; // = kTotal - 4 - 8
52+
53+
CoinbaseLayout make_layout()
54+
{
55+
CoinbaseLayout lay;
56+
lay.bytes.resize(kTotal);
57+
std::iota(lay.bytes.begin(), lay.bytes.end(), static_cast<unsigned char>(0));
58+
for (size_t i = kOffset; i < kOffset + EXTRANONCE2_SIZE; ++i)
59+
lay.bytes[i] = 0x00;
60+
lay.nonce64_offset = kOffset;
61+
lay.ref_hash_offset = kOffset - 32;
62+
return lay;
63+
}
64+
65+
std::vector<unsigned char> reassemble(const CoinbaseLayout& lay,
66+
std::span<const unsigned char> e2)
67+
{
68+
std::vector<unsigned char> cb;
69+
cb.reserve(lay.bytes.size());
70+
cb.insert(cb.end(), lay.bytes.begin(), lay.bytes.begin() + lay.nonce64_offset);
71+
cb.insert(cb.end(), e2.begin(), e2.end());
72+
cb.insert(cb.end(), lay.bytes.begin() + lay.nonce64_offset + EXTRANONCE2_SIZE,
73+
lay.bytes.end());
74+
return cb;
75+
}
76+
77+
uint256 hash_of(const std::vector<unsigned char>& b)
78+
{
79+
return sha256d(std::span<const unsigned char>(b.data(), b.size()));
80+
}
81+
82+
// Synthetic sibling tx hashes (indices 1..3 of the merkle tree). Values are
83+
// arbitrary but fixed; the Python oracle uses the same sha256d(bytes([v]*4)).
84+
uint256 mk_tx(unsigned char v)
85+
{
86+
std::vector<unsigned char> b(4, v);
87+
return sha256d(std::span<const unsigned char>(b.data(), b.size()));
88+
}
89+
90+
// Pool-side canonical merkle root over [coinbase_leaf, h1, h2, h3] -- computed
91+
// in-test, independent of merkle_branches_raw, so the round-trip check is not a
92+
// tautology against the SUT branch.
93+
uint256 canonical_root(uint256 leaf0, uint256 h1, uint256 h2, uint256 h3)
94+
{
95+
std::vector<uint256> layer{leaf0, h1, h2, h3};
96+
while (layer.size() > 1) {
97+
if (layer.size() % 2 == 1) layer.push_back(layer.back());
98+
std::vector<uint256> next;
99+
for (size_t i = 0; i + 1 < layer.size(); i += 2) {
100+
std::vector<unsigned char> buf(64);
101+
std::memcpy(buf.data(), layer[i].data(), 32);
102+
std::memcpy(buf.data() + 32, layer[i + 1].data(), 32);
103+
next.push_back(sha256d(std::span<const unsigned char>(buf.data(), buf.size())));
104+
}
105+
layer.swap(next);
106+
}
107+
return layer[0];
108+
}
109+
110+
// Miner-side walk: fold the coinbase leaf through the branch siblings, exactly
111+
// as cpuminer does with the mining.notify merkle_branch. root = leaf; for each
112+
// sibling s: root = sha256d(root || s).
113+
uint256 miner_root(uint256 leaf0, const std::vector<uint256>& branch)
114+
{
115+
uint256 r = leaf0;
116+
for (const auto& s : branch) {
117+
std::vector<unsigned char> buf(64);
118+
std::memcpy(buf.data(), r.data(), 32);
119+
std::memcpy(buf.data() + 32, s.data(), 32);
120+
r = sha256d(std::span<const unsigned char>(buf.data(), buf.size()));
121+
}
122+
return r;
123+
}
124+
125+
const unsigned char E2_A[8] = {1, 2, 3, 4, 5, 6, 7, 8};
126+
const unsigned char E2_B[8] = {0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xf0, 0x0d};
127+
const unsigned char E2_ZERO[8] = {0, 0, 0, 0, 0, 0, 0, 0};
128+
129+
// The branch the pool computes ONCE per job, from the placeholder coinbase
130+
// (slot 0 == zero-hash) plus the three sibling tx hashes. Siblings are
131+
// independent of leaf 0, which is exactly what makes the branch reusable.
132+
std::vector<uint256> job_branch()
133+
{
134+
std::vector<uint256> leaves{uint256::ZERO, mk_tx(0xa1), mk_tx(0xb2), mk_tx(0xc3)};
135+
return merkle_branches_raw(leaves);
136+
}
137+
138+
} // namespace
139+
140+
// (1) The notify branch has the expected depth for a 4-leaf tree: two siblings
141+
// (one per merkle level). A wrong-depth branch silently mis-binds.
142+
TEST(DashJobNotifyRoundTrip, BranchDepth)
143+
{
144+
EXPECT_EQ(job_branch().size(), static_cast<size_t>(2));
145+
}
146+
147+
// (2) ROUND-TRIP core: for the zero extranonce2 (identity), the miner walk over
148+
// the notify branch reproduces the pool canonical root byte-for-byte.
149+
TEST(DashJobNotifyRoundTrip, ZeroNonceRootMatches)
150+
{
151+
CoinbaseLayout lay = make_layout();
152+
uint256 h1 = mk_tx(0xa1), h2 = mk_tx(0xb2), h3 = mk_tx(0xc3);
153+
uint256 leaf0 = hash_of(reassemble(lay, std::span<const unsigned char>(E2_ZERO, 8)));
154+
EXPECT_EQ(miner_root(leaf0, job_branch()), canonical_root(leaf0, h1, h2, h3));
155+
}
156+
157+
// (3) ROUND-TRIP under extranonce2 iteration: the SAME job branch binds every
158+
// distinct extranonce2 back to the matching canonical root -- the miner
159+
// iterates the slot without re-fetching the job.
160+
TEST(DashJobNotifyRoundTrip, IteratedNonceRootMatches)
161+
{
162+
CoinbaseLayout lay = make_layout();
163+
uint256 h1 = mk_tx(0xa1), h2 = mk_tx(0xb2), h3 = mk_tx(0xc3);
164+
const std::vector<uint256> br = job_branch();
165+
for (const auto* e2 : {E2_A, E2_B}) {
166+
uint256 leaf0 = hash_of(reassemble(lay, std::span<const unsigned char>(e2, 8)));
167+
EXPECT_EQ(miner_root(leaf0, br), canonical_root(leaf0, h1, h2, h3));
168+
}
169+
}
170+
171+
// (4) Distinct extranonce2 -> distinct block merkle root: the notify branch
172+
// PARTITIONS the search space rather than collapsing it (overlapping roots
173+
// would let two miners claim one solution).
174+
TEST(DashJobNotifyRoundTrip, RootInjectivity)
175+
{
176+
CoinbaseLayout lay = make_layout();
177+
const std::vector<uint256> br = job_branch();
178+
uint256 r0 = miner_root(hash_of(reassemble(lay, std::span<const unsigned char>(E2_ZERO, 8))), br);
179+
uint256 rA = miner_root(hash_of(reassemble(lay, std::span<const unsigned char>(E2_A, 8))), br);
180+
uint256 rB = miner_root(hash_of(reassemble(lay, std::span<const unsigned char>(E2_B, 8))), br);
181+
EXPECT_NE(rA, rB);
182+
EXPECT_NE(rA, r0);
183+
EXPECT_NE(rB, r0);
184+
}
185+
186+
// (5) Wire form of the merkle branch is LE internal-bytes hex (stratum.py
187+
// convention), NOT the reversed display form. Golden hex from independent
188+
// Python (pack.IntType(256).pack(s).encode(hex)).
189+
TEST(DashJobNotifyRoundTrip, BranchWireHexGolden)
190+
{
191+
std::vector<std::string> hex = merkle_branches_hex(job_branch());
192+
ASSERT_EQ(hex.size(), static_cast<size_t>(2));
193+
EXPECT_EQ(hex[0], "f8401cfffc19b3a56c2de52278297152d9028b4efbc30f8d2096f780ceb0b2b6");
194+
EXPECT_EQ(hex[1], "6f43268501a527c6adc37e7d6a293168c42f9aae863ab15628ab78ddf9ec836c");
195+
}
196+
197+
// (6) Golden merkle-root byte-parity anchors (GetHex display form), computed
198+
// independently in Python over [sha256d(coinbase_e2), h1, h2, h3]:
199+
// h_i = sha256d(bytes([0xa1/0xb2/0xc3]*4))
200+
// leaf0 = sha256d(base[:28] + e2 + base[36:]), base = bytes(range(40)) w/ [28:36]=0
201+
TEST(DashJobNotifyRoundTrip, CanonicalRootGolden)
202+
{
203+
CoinbaseLayout lay = make_layout();
204+
uint256 h1 = mk_tx(0xa1), h2 = mk_tx(0xb2), h3 = mk_tx(0xc3);
205+
auto root_for = [&](const unsigned char* e2) {
206+
return canonical_root(
207+
hash_of(reassemble(lay, std::span<const unsigned char>(e2, 8))), h1, h2, h3);
208+
};
209+
EXPECT_EQ(root_for(E2_ZERO).GetHex(),
210+
"b4bfbae433a5f984c30f4bead3cfe650a0e770bd8126b5c16a15b4b5b9243189");
211+
EXPECT_EQ(root_for(E2_A).GetHex(),
212+
"d445471556d06a5b2722c27f92498bf030117d756fd8fe7997d3732cf4df5e93");
213+
EXPECT_EQ(root_for(E2_B).GetHex(),
214+
"7c81c0a061417d008d0ada06bfe98d69d82ea6561ec1c327f7fb34d63259772c");
215+
}

0 commit comments

Comments
 (0)