|
| 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