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