Skip to content

Commit d4ccd36

Browse files
committed
test(dash-s8): mining.submit reassembly KAT — close subscribe→notify→submit triangle
Bind the REAL landed producers split_coinb() + merkle_branches_raw() to prove a miner submit (extranonce2, ntime, nonce) reconstructs the EXACT 80-byte block header whose sha256d is the share hash. Reassembled coinbase cross-checks the Python sha256d anchors; extranonce2/ntime/nonce each shown to bind injectively into the share. Fenced test/ + build.yml only, non-consensus, socket/node-free.
1 parent 6249d2c commit d4ccd36

3 files changed

Lines changed: 286 additions & 1 deletion

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ jobs:
102102
core_test sharechain_test share_test btc_share_test \
103103
test_threading test_weights \
104104
test_header_chain test_mempool test_template_builder \
105-
test_doge_chain test_compact_blocks test_dash_x11_kat \
105+
test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_stratum_submit_reassembly \
106106
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 \
107107
test_multiaddress_pplns test_pplns_stress \
108108
test_hash_link test_decay_pplns \

test/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,15 @@ if (BUILD_TESTING AND GTest_FOUND)
729729
)
730730
target_link_libraries(test_dash_stratum_extranonce_split PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
731731
gtest_add_tests(test_dash_stratum_extranonce_split "" AUTO)
732+
add_executable(test_dash_stratum_submit_reassembly test_dash_stratum_submit_reassembly.cpp)
733+
target_link_libraries(test_dash_stratum_submit_reassembly PRIVATE
734+
GTest::gtest_main GTest::gtest
735+
dash_x11 core
736+
nlohmann_json::nlohmann_json
737+
${Boost_LIBRARIES}
738+
)
739+
target_link_libraries(test_dash_stratum_submit_reassembly PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
740+
gtest_add_tests(test_dash_stratum_submit_reassembly "" AUTO)
732741

733742
add_executable(test_compact_blocks test_compact_blocks.cpp)
734743
target_link_libraries(test_compact_blocks PRIVATE
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// SPDX-License-Identifier: MIT
2+
//
3+
// DASH S8 stratum mining.submit reassembly contract KAT.
4+
//
5+
// Closes the subscribe -> notify -> submit triangle. The prior leaves pinned
6+
// two of the three sides:
7+
// * #630 test_dash_stratum_binding -- get_work extranonce2 slot geometry
8+
// * #631 test_dash_stratum_notify_roundtrip -- notify merkle_branch fold -> merkle_root
9+
// * #633 test_dash_stratum_extranonce_split -- subscribe coinb1/coinb2 split
10+
// The OPEN side is submit: given a miner's (extranonce2, ntime, nonce), the
11+
// server must reconstruct the EXACT block header whose sha256d IS the share
12+
// hash. This leaf proves that reconstruction end-to-end:
13+
//
14+
// mining.submit [extranonce2, ntime, nonce]
15+
// -> reassemble coinb1 || extranonce2 || coinb2 (REAL split_coinb)
16+
// -> sha256d -> coinbase txid (== #631/#633 golden)
17+
// -> fold notify merkle_branch (index 0) (REAL merkle_branches_raw)
18+
// -> header merkle_root
19+
// -> assemble 80B header [version|prev|root|ntime|nbits|nonce]
20+
// -> sha256d -> the share/header hash
21+
//
22+
// It binds the REAL landed producers dash::coinbase::split_coinb() and
23+
// dash::coinbase::merkle_branches_raw() (src/impl/dash/coinbase_builder.hpp) --
24+
// the exact code mining.subscribe/mining.notify ship -- and mirrors the
25+
// miner-side reassembly + fold + header assembly locally (the miner is
26+
// cpuminer, not our code; handle_submit's compute_share_difficulty walks the
27+
// identical reconstruction, src/core/stratum_server.cpp:877).
28+
//
29+
// The reconstructed coinbase is cross-checked byte-for-byte against the #631 /
30+
// #633 golden coinbase hashes, and the header hash is pinned to independent
31+
// Python hashlib sha256d anchors (NOT the oracle code, NOT this code) -- so the
32+
// submit reconstruction agrees with the subscribe/notify producers on the
33+
// coinbase, and the header hash is non-circular. extranonce2, ntime, and nonce
34+
// are each shown to bind injectively into the header hash -- i.e. all three
35+
// miner-supplied submit fields are load-bearing in the share the pool verifies.
36+
//
37+
// Oracle (frstrtr/p2pool-dash @9a0a609):
38+
// p2pool/work.py:474 header['merkle_root'] == check_merkle_link(hash256(gentx), link)
39+
// p2pool/dash/data.py block_header_type: version|prev|merkle_root|timestamp|bits|nonce
40+
// p2pool/dash/stratum.py rpc_submit [worker, job_id, extranonce2, ntime, nonce]
41+
//
42+
// Fenced: test/ + build.yml allowlist only. Non-consensus, socket-free,
43+
// node-free -- pure synthetic CoinbaseLayout, no live node / RPC / P2P.
44+
45+
#include <gtest/gtest.h>
46+
47+
#include <cstddef>
48+
#include <cstdint>
49+
#include <cstring>
50+
#include <span>
51+
#include <string>
52+
#include <vector>
53+
54+
#include <impl/dash/coinbase_builder.hpp> // CoinbaseLayout, split_coinb, merkle_branches_raw, sha256d, EXTRANONCE2_SIZE
55+
#include <btclibs/util/strencodings.h> // HexStr, ParseHex
56+
#include <core/uint256.hpp>
57+
58+
using dash::coinbase::CoinbaseLayout;
59+
using dash::coinbase::CoinbSplit;
60+
using dash::coinbase::split_coinb;
61+
using dash::coinbase::merkle_branches_raw;
62+
using dash::coinbase::sha256d;
63+
using dash::coinbase::EXTRANONCE2_SIZE;
64+
65+
namespace {
66+
67+
// ── Coinbase fixture (identical to #630/#631/#633) ───────────────────────────
68+
// 40-byte synthetic coinbase: bytes[i] = i, with the 8-byte nonce64
69+
// (extranonce2) slot at [28,36). nonce64_offset = 40 - locktime(4) - nonce64(8).
70+
constexpr size_t kTotal = 40;
71+
constexpr size_t kNonceOffset = 28;
72+
73+
CoinbaseLayout make_layout() {
74+
CoinbaseLayout lay;
75+
lay.bytes.resize(kTotal);
76+
for (size_t i = 0; i < kTotal; ++i) lay.bytes[i] = static_cast<unsigned char>(i);
77+
for (size_t i = kNonceOffset; i < kNonceOffset + EXTRANONCE2_SIZE; ++i) lay.bytes[i] = 0;
78+
lay.ref_hash_offset = kNonceOffset - 32;
79+
lay.nonce64_offset = kNonceOffset;
80+
return lay;
81+
}
82+
83+
// Miner-side reassembly: coinb1 || extranonce2 || coinb2 (mirror of cpuminer).
84+
std::vector<unsigned char> reassemble(const CoinbSplit& s, std::span<const unsigned char> e2) {
85+
std::vector<unsigned char> b1 = ParseHex(s.coinb1_hex);
86+
std::vector<unsigned char> b2 = ParseHex(s.coinb2_hex);
87+
std::vector<unsigned char> out;
88+
out.reserve(b1.size() + e2.size() + b2.size());
89+
out.insert(out.end(), b1.begin(), b1.end());
90+
out.insert(out.end(), e2.begin(), e2.end());
91+
out.insert(out.end(), b2.begin(), b2.end());
92+
return out;
93+
}
94+
95+
uint256 coinbase_hash(const CoinbSplit& s, std::span<const unsigned char> e2) {
96+
auto cb = reassemble(s, e2);
97+
return sha256d(std::span<const unsigned char>(cb.data(), cb.size()));
98+
}
99+
100+
uint256 fill_hash(unsigned char b) { return uint256(std::vector<unsigned char>(32, b)); }
101+
102+
// Four-leaf tx set: coinbase placeholder at [0] + three sibling tx hashes
103+
// (identical to #631, so the merkle_branch is the exact one mining.notify ships).
104+
std::vector<uint256> leaves_with(const uint256& cb) {
105+
return { cb, fill_hash(0x11), fill_hash(0x22), fill_hash(0x33) };
106+
}
107+
108+
// sha256d(left || right), 32B internal LE each -- mirrors merkle_record_type.pack.
109+
uint256 node_hash(const uint256& l, const uint256& r) {
110+
std::vector<unsigned char> buf(64);
111+
auto lc = l.GetChars();
112+
auto rc = r.GetChars();
113+
std::memcpy(buf.data(), lc.data(), 32);
114+
std::memcpy(buf.data() + 32, rc.data(), 32);
115+
return sha256d(std::span<const unsigned char>(buf.data(), buf.size()));
116+
}
117+
118+
// Miner-side fold for coinbase leaf index 0: running hash always on the LEFT.
119+
uint256 fold_index0(const uint256& tip, const std::vector<uint256>& branch) {
120+
uint256 cur = tip;
121+
for (const auto& sib : branch) cur = node_hash(cur, sib);
122+
return cur;
123+
}
124+
125+
// ── 80-byte block-header fields (synthetic) ──────────────────────────────────
126+
constexpr uint32_t VERSION = 0x20000000u;
127+
constexpr uint32_t NBITS = 0x1a0ffff0u;
128+
constexpr uint32_t NTIME = 0x6512a3f4u;
129+
constexpr uint32_t NTIME2 = 0x6512a400u; // distinct ntime (binding guard)
130+
constexpr uint32_t NONCE = 0x0000abcdu;
131+
constexpr uint32_t NONCE2 = 0x0000abceu; // distinct nonce (binding guard)
132+
const char* PREV_DISPLAY =
133+
"00000000000000000000000000000000000000000000000000000000deadbeef";
134+
135+
void put_le32(std::vector<unsigned char>& b, uint32_t v) {
136+
b.push_back(static_cast<unsigned char>( v & 0xff));
137+
b.push_back(static_cast<unsigned char>((v >> 8) & 0xff));
138+
b.push_back(static_cast<unsigned char>((v >> 16) & 0xff));
139+
b.push_back(static_cast<unsigned char>((v >> 24) & 0xff));
140+
}
141+
142+
// Assemble the canonical 80-byte header and return its sha256d (the share hash).
143+
// Layout: version(4 LE) | prev(32 internal LE) | merkle_root(32 internal LE) |
144+
// ntime(4 LE) | nbits(4 LE) | nonce(4 LE).
145+
uint256 header_hash(const uint256& merkle_root, uint32_t ntime, uint32_t nonce) {
146+
std::vector<unsigned char> h;
147+
h.reserve(80);
148+
put_le32(h, VERSION);
149+
auto prev = uint256S(PREV_DISPLAY).GetChars(); // display -> internal LE
150+
h.insert(h.end(), prev.begin(), prev.end());
151+
auto root = merkle_root.GetChars();
152+
h.insert(h.end(), root.begin(), root.end());
153+
put_le32(h, ntime);
154+
put_le32(h, NBITS);
155+
put_le32(h, nonce);
156+
EXPECT_EQ(h.size(), 80u);
157+
return sha256d(std::span<const unsigned char>(h.data(), h.size()));
158+
}
159+
160+
// Full reconstruction from a miner submit: (extranonce2, ntime, nonce) -> share.
161+
uint256 reconstruct_share(const CoinbSplit& s, std::span<const unsigned char> e2,
162+
uint32_t ntime, uint32_t nonce) {
163+
uint256 cb = coinbase_hash(s, e2);
164+
auto branch = merkle_branches_raw(leaves_with(cb));
165+
uint256 root = fold_index0(cb, branch);
166+
return header_hash(root, ntime, nonce);
167+
}
168+
169+
const std::vector<unsigned char> E2_ZERO(8, 0x00);
170+
const std::vector<unsigned char> E2_A{0xA1,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8};
171+
const std::vector<unsigned char> E2_B{0xB1,0xB2,0xB3,0xB4,0xB5,0xB6,0xB7,0xB8};
172+
173+
// Cross-bind anchors: coinbase hashes identical to #631/#633 (independent Python).
174+
const char* GOLD_CBHASH_Z = "b0d22de8e7f6765f9d8c289ecd77ad8e0ab6a88c2a4ccf594c509d7775bb54ab";
175+
const char* GOLD_CBHASH_A = "22a146527ac0731341d026480042a55cee0bdb804043b570af24feb9e76f2c8b";
176+
const char* GOLD_CBHASH_B = "04c8f4793537be307eaa2166642accc8265add0a59ccf4d01525f0ec54c6237a";
177+
178+
// Merkle roots identical to #631 GOLD_ROOT_* (the notify leaf ships this branch).
179+
const char* GOLD_ROOT_Z = "26f79b540fe6f2ac6fc52f37e5af5b33a61f33216bc4ec394635b24235850ea0";
180+
const char* GOLD_ROOT_A = "4b2d0948edb39954294fd28ca3926a78f0e56990c69d4fd7530a80a40491198e";
181+
const char* GOLD_ROOT_B = "72f7c45d5d6c65fd77053fec86d20d918681b92aba88c87f5aaa5bf7ab3a8230";
182+
183+
// 80-byte header-hash anchors (independent Python hashlib sha256d; display form).
184+
const char* GOLD_HDR_Z = "fee1d073bb4d6329a1dcc557806bde0856353ed9cc564278fb64e7c786174fe8";
185+
const char* GOLD_HDR_A = "6549fd53a623d482003f58bd9b309b97b86f749c6b0480eb3c74ab29fbc7058b";
186+
const char* GOLD_HDR_B = "1652fb9178ae66f2edb802bf9b7c3c316e2f80b1dc4bcd21cfc07386bde33742";
187+
const char* GOLD_HDR_NTIME2 = "8dcaed61b0b47c637316436355030b3927f98042d35bc31784e5fd81a15d2495";
188+
const char* GOLD_HDR_NONCE2 = "ebc6423bb7454bd964bd07e06fab55c4dad5af62be9f26949d3c39edcd5f0563";
189+
190+
} // namespace
191+
192+
// (1) Submit reconstructs the golden coinbase: reassembling coinb1 (from the
193+
// REAL split_coinb producer) || extranonce2 || coinb2 and sha256d'ing it
194+
// reproduces the EXACT coinbase hash the notify/split leaves golden-anchor
195+
// -- the submit path agrees with subscribe/notify byte-for-byte.
196+
TEST(DashStratumSubmitReassembly, SubmitReconstructsGoldenCoinbase) {
197+
CoinbSplit s = split_coinb(make_layout());
198+
EXPECT_EQ(coinbase_hash(s, E2_ZERO).GetHex(), GOLD_CBHASH_Z);
199+
EXPECT_EQ(coinbase_hash(s, E2_A).GetHex(), GOLD_CBHASH_A);
200+
EXPECT_EQ(coinbase_hash(s, E2_B).GetHex(), GOLD_CBHASH_B);
201+
}
202+
203+
// (2) Reconstructed merkle_root matches the #631 notify leaf: folding the REAL
204+
// mining.notify branch over the reassembled coinbase reproduces the exact
205+
// header merkle_root the notify round-trip pins.
206+
TEST(DashStratumSubmitReassembly, ReconstructedRootMatchesNotifyLeaf) {
207+
CoinbSplit s = split_coinb(make_layout());
208+
for (auto [e2, gold] : std::vector<std::pair<std::span<const unsigned char>, const char*>>{
209+
{E2_ZERO, GOLD_ROOT_Z}, {E2_A, GOLD_ROOT_A}, {E2_B, GOLD_ROOT_B}}) {
210+
uint256 cb = coinbase_hash(s, e2);
211+
auto br = merkle_branches_raw(leaves_with(cb));
212+
EXPECT_EQ(fold_index0(cb, br).GetHex(), gold);
213+
}
214+
}
215+
216+
// (3) THE closing assertion: the full submit reconstruction -- (extranonce2,
217+
// ntime, nonce) -> reassembled coinbase -> folded merkle_root -> 80B header
218+
// -> sha256d -- yields the exact share hash pinned by an independent Python
219+
// golden. The subscribe->notify->submit triangle is closed.
220+
TEST(DashStratumSubmitReassembly, ReconstructedHeaderHashesToShare) {
221+
CoinbSplit s = split_coinb(make_layout());
222+
EXPECT_EQ(reconstruct_share(s, E2_ZERO, NTIME, NONCE).GetHex(), GOLD_HDR_Z);
223+
}
224+
225+
// (4) extranonce2 binds into the share: distinct extranonce2 -> distinct header
226+
// hash, each equal to its independent golden. The miner's nonce64 slot
227+
// propagates all the way into the share the pool verifies.
228+
TEST(DashStratumSubmitReassembly, Extranonce2BindsIntoShareHash) {
229+
CoinbSplit s = split_coinb(make_layout());
230+
uint256 z = reconstruct_share(s, E2_ZERO, NTIME, NONCE);
231+
uint256 a = reconstruct_share(s, E2_A, NTIME, NONCE);
232+
uint256 b = reconstruct_share(s, E2_B, NTIME, NONCE);
233+
EXPECT_EQ(a.GetHex(), GOLD_HDR_A);
234+
EXPECT_EQ(b.GetHex(), GOLD_HDR_B);
235+
EXPECT_NE(z, a);
236+
EXPECT_NE(z, b);
237+
EXPECT_NE(a, b);
238+
}
239+
240+
// (5) ntime binds into the share: same extranonce2/nonce, distinct ntime ->
241+
// distinct header hash. ntime is a load-bearing submit field, not decor.
242+
TEST(DashStratumSubmitReassembly, NtimeBindsIntoShareHash) {
243+
CoinbSplit s = split_coinb(make_layout());
244+
uint256 base = reconstruct_share(s, E2_ZERO, NTIME, NONCE);
245+
uint256 alt = reconstruct_share(s, E2_ZERO, NTIME2, NONCE);
246+
EXPECT_EQ(base.GetHex(), GOLD_HDR_Z);
247+
EXPECT_EQ(alt.GetHex(), GOLD_HDR_NTIME2);
248+
EXPECT_NE(base, alt);
249+
}
250+
251+
// (6) nonce binds into the share: same extranonce2/ntime, distinct nonce ->
252+
// distinct header hash. The header nonce field is placed correctly.
253+
TEST(DashStratumSubmitReassembly, NonceBindsIntoShareHash) {
254+
CoinbSplit s = split_coinb(make_layout());
255+
uint256 base = reconstruct_share(s, E2_ZERO, NTIME, NONCE);
256+
uint256 alt = reconstruct_share(s, E2_ZERO, NTIME, NONCE2);
257+
EXPECT_EQ(base.GetHex(), GOLD_HDR_Z);
258+
EXPECT_EQ(alt.GetHex(), GOLD_HDR_NONCE2);
259+
EXPECT_NE(base, alt);
260+
}
261+
262+
// (7) Golden byte-parity anchors (independent Python sha256d, computed outside
263+
// both the oracle and this code) for coinbase, merkle_root, and header hash.
264+
TEST(DashStratumSubmitReassembly, GoldenAnchors) {
265+
CoinbSplit s = split_coinb(make_layout());
266+
// coinbase (cross-bind to #631/#633)
267+
EXPECT_EQ(coinbase_hash(s, E2_ZERO).GetHex(), GOLD_CBHASH_Z);
268+
EXPECT_EQ(coinbase_hash(s, E2_A).GetHex(), GOLD_CBHASH_A);
269+
EXPECT_EQ(coinbase_hash(s, E2_B).GetHex(), GOLD_CBHASH_B);
270+
// header shares
271+
EXPECT_EQ(reconstruct_share(s, E2_ZERO, NTIME, NONCE ).GetHex(), GOLD_HDR_Z);
272+
EXPECT_EQ(reconstruct_share(s, E2_A, NTIME, NONCE ).GetHex(), GOLD_HDR_A);
273+
EXPECT_EQ(reconstruct_share(s, E2_B, NTIME, NONCE ).GetHex(), GOLD_HDR_B);
274+
EXPECT_EQ(reconstruct_share(s, E2_ZERO, NTIME2, NONCE ).GetHex(), GOLD_HDR_NTIME2);
275+
EXPECT_EQ(reconstruct_share(s, E2_ZERO, NTIME, NONCE2).GetHex(), GOLD_HDR_NONCE2);
276+
}

0 commit comments

Comments
 (0)