|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +// |
| 3 | +// DASH S8 stratum extranonce coinb1/coinb2 split contract KAT. |
| 4 | +// |
| 5 | +// Pins the mining.subscribe + mining.notify wire *split* that the get_work() |
| 6 | +// slot KAT (test_dash_stratum_binding) and the notify merkle KAT |
| 7 | +// (test_dash_stratum_notify_roundtrip) do not: the server ships the coinbase to |
| 8 | +// the miner as two hex halves -- coinb1 || <extranonce2> || coinb2 -- and |
| 9 | +// advertises an extranonce2_size the miner must fill EXACTLY. This leaf binds |
| 10 | +// the REAL landed producer dash::coinbase::split_coinb() |
| 11 | +// (src/impl/dash/coinbase_builder.hpp) and proves: |
| 12 | +// * the split leaves a gap of exactly EXTRANONCE2_SIZE (==8) bytes between |
| 13 | +// coinb1 and coinb2 -- i.e. the advertised extranonce2_size the server |
| 14 | +// returns in mining.subscribe equals the reserved nonce64 slot width; |
| 15 | +// * coinb1 == bytes[0 : nonce64_offset], coinb2 == bytes[nonce64_offset+8 :] |
| 16 | +// (no overlap with the slot, no gap, no truncation); |
| 17 | +// * reassembling coinb1 || extranonce2 || coinb2 reproduces the EXACT coinbase |
| 18 | +// whose sha256d the slot-substitution KATs golden-anchor -- so the split |
| 19 | +// producer and the slot-substitution producer agree byte-for-byte; |
| 20 | +// * distinct extranonce2 values propagate injectively through the reassembly; |
| 21 | +// * a wrong-width extranonce2 (7 or 9 bytes) reassembles to a coinbase of the |
| 22 | +// wrong length -- the advertised size is load-bearing, not decorative. |
| 23 | +// |
| 24 | +// Oracle (frstrtr/p2pool-dash @9a0a609): |
| 25 | +// p2pool/dash/stratum.py rpc_subscribe returns |
| 26 | +// [subscription_details, extranonce1, extranonce2_size(=COINBASE_NONCE_LENGTH)] |
| 27 | +// -- the size the miner must fill; here COINBASE_NONCE_LENGTH == 8. |
| 28 | +// p2pool/work.py get_work -> coinb1/coinb2 hex around the extranonce slot. |
| 29 | +// |
| 30 | +// Non-circular: the coinbase-hash golden anchors are computed by independent |
| 31 | +// Python hashlib sha256d (NOT the oracle code and NOT split_coinb) -- identical |
| 32 | +// to the anchors the notify-roundtrip leaf pins, cross-binding the two |
| 33 | +// producers. The coinb1/coinb2 hex anchors are the raw fixture bytes |
| 34 | +// (independently enumerable). |
| 35 | +// |
| 36 | +// Fenced: test/ + build.yml allowlist only. Non-consensus, socket-free, |
| 37 | +// node-free -- pure synthetic CoinbaseLayout, no live node / RPC / P2P. |
| 38 | + |
| 39 | +#include <gtest/gtest.h> |
| 40 | + |
| 41 | +#include <cstddef> |
| 42 | +#include <span> |
| 43 | +#include <stdexcept> |
| 44 | +#include <string> |
| 45 | +#include <vector> |
| 46 | + |
| 47 | +#include <impl/dash/coinbase_builder.hpp> // CoinbaseLayout, CoinbSplit, split_coinb, sha256d, EXTRANONCE2_SIZE |
| 48 | +#include <btclibs/util/strencodings.h> // HexStr, ParseHex |
| 49 | +#include <core/uint256.hpp> |
| 50 | + |
| 51 | +using dash::coinbase::CoinbaseLayout; |
| 52 | +using dash::coinbase::CoinbSplit; |
| 53 | +using dash::coinbase::split_coinb; |
| 54 | +using dash::coinbase::sha256d; |
| 55 | +using dash::coinbase::EXTRANONCE2_SIZE; |
| 56 | + |
| 57 | +namespace { |
| 58 | + |
| 59 | +// Same 40-byte synthetic coinbase as the slot/notify KATs: bytes[i] = i, with |
| 60 | +// the 8-byte nonce64 (extranonce2) slot at [28,36). nonce64_offset = |
| 61 | +// 40 - locktime(4) - nonce64(8) = 28. |
| 62 | +constexpr size_t kTotal = 40; |
| 63 | +constexpr size_t kNonceOffset = 28; |
| 64 | + |
| 65 | +CoinbaseLayout make_layout() { |
| 66 | + CoinbaseLayout lay; |
| 67 | + lay.bytes.resize(kTotal); |
| 68 | + for (size_t i = 0; i < kTotal; ++i) lay.bytes[i] = static_cast<unsigned char>(i); |
| 69 | + for (size_t i = kNonceOffset; i < kNonceOffset + EXTRANONCE2_SIZE; ++i) lay.bytes[i] = 0; |
| 70 | + lay.ref_hash_offset = kNonceOffset - 32; // 32B ref_hash region precedes the slot |
| 71 | + lay.nonce64_offset = kNonceOffset; |
| 72 | + return lay; |
| 73 | +} |
| 74 | + |
| 75 | +// Reassemble coinb1 || extranonce2 || coinb2 exactly as a miner does, returning |
| 76 | +// the raw bytes. e2 may be any width (used by the wrong-width guard). |
| 77 | +std::vector<unsigned char> reassemble(const CoinbSplit& s, std::span<const unsigned char> e2) { |
| 78 | + std::vector<unsigned char> b1 = ParseHex(s.coinb1_hex); |
| 79 | + std::vector<unsigned char> b2 = ParseHex(s.coinb2_hex); |
| 80 | + std::vector<unsigned char> out; |
| 81 | + out.reserve(b1.size() + e2.size() + b2.size()); |
| 82 | + out.insert(out.end(), b1.begin(), b1.end()); |
| 83 | + out.insert(out.end(), e2.begin(), e2.end()); |
| 84 | + out.insert(out.end(), b2.begin(), b2.end()); |
| 85 | + return out; |
| 86 | +} |
| 87 | + |
| 88 | +uint256 reassembled_hash(const CoinbSplit& s, std::span<const unsigned char> e2) { |
| 89 | + auto cb = reassemble(s, e2); |
| 90 | + return sha256d(std::span<const unsigned char>(cb.data(), cb.size())); |
| 91 | +} |
| 92 | + |
| 93 | +const std::vector<unsigned char> E2_ZERO(8, 0x00); |
| 94 | +const std::vector<unsigned char> E2_A{0xA1,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8}; |
| 95 | +const std::vector<unsigned char> E2_B{0xB1,0xB2,0xB3,0xB4,0xB5,0xB6,0xB7,0xB8}; |
| 96 | + |
| 97 | +// Independent fixture-byte anchors (coinb1 = bytes 0x00..0x1b, coinb2 = 0x24..0x27). |
| 98 | +const char* GOLD_COINB1 = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b"; |
| 99 | +const char* GOLD_COINB2 = "24252627"; |
| 100 | + |
| 101 | +// Independent Python hashlib sha256d anchors (display/GetHex form) -- identical |
| 102 | +// to the coinbase-hash goldens the notify-roundtrip leaf pins, cross-binding |
| 103 | +// split_coinb to the slot-substitution producer. |
| 104 | +const char* GOLD_CBHASH_Z = "b0d22de8e7f6765f9d8c289ecd77ad8e0ab6a88c2a4ccf594c509d7775bb54ab"; |
| 105 | +const char* GOLD_CBHASH_A = "22a146527ac0731341d026480042a55cee0bdb804043b570af24feb9e76f2c8b"; |
| 106 | +const char* GOLD_CBHASH_B = "04c8f4793537be307eaa2166642accc8265add0a59ccf4d01525f0ec54c6237a"; |
| 107 | + |
| 108 | +} // namespace |
| 109 | + |
| 110 | +// (1) Split geometry: coinb1 ends at the slot, coinb2 begins after it, and the |
| 111 | +// gap between them is EXACTLY EXTRANONCE2_SIZE bytes -- the advertised |
| 112 | +// extranonce2_size the server returns in mining.subscribe. |
| 113 | +TEST(DashStratumExtranonceSplit, SplitLeavesExactSlotGap) { |
| 114 | + auto lay = make_layout(); |
| 115 | + CoinbSplit s = split_coinb(lay); |
| 116 | + const size_t b1 = s.coinb1_hex.size() / 2; |
| 117 | + const size_t b2 = s.coinb2_hex.size() / 2; |
| 118 | + EXPECT_EQ(b1, kNonceOffset); // coinb1 == bytes[0:offset] |
| 119 | + EXPECT_EQ(b2, kTotal - kNonceOffset - EXTRANONCE2_SIZE); |
| 120 | + // The gap the miner must fill == the advertised extranonce2_size == slot width. |
| 121 | + EXPECT_EQ(kTotal - b1 - b2, EXTRANONCE2_SIZE); |
| 122 | + EXPECT_EQ(EXTRANONCE2_SIZE, 8u); // == oracle COINBASE_NONCE_LENGTH |
| 123 | +} |
| 124 | + |
| 125 | +// (2) coinb1 / coinb2 are the exact fixture halves around the slot (byte-parity). |
| 126 | +TEST(DashStratumExtranonceSplit, HalvesAreExactBytesAroundSlot) { |
| 127 | + CoinbSplit s = split_coinb(make_layout()); |
| 128 | + EXPECT_EQ(s.coinb1_hex, GOLD_COINB1); |
| 129 | + EXPECT_EQ(s.coinb2_hex, GOLD_COINB2); |
| 130 | +} |
| 131 | + |
| 132 | +// (3) Reassembly contract: coinb1 || extranonce2 || coinb2 reproduces the EXACT |
| 133 | +// coinbase whose sha256d the slot/notify KATs golden-anchor -- the split |
| 134 | +// producer and the slot-substitution producer agree byte-for-byte. |
| 135 | +TEST(DashStratumExtranonceSplit, ReassemblyReproducesGoldenCoinbase) { |
| 136 | + CoinbSplit s = split_coinb(make_layout()); |
| 137 | + EXPECT_EQ(reassembled_hash(s, E2_ZERO).GetHex(), GOLD_CBHASH_Z); |
| 138 | + EXPECT_EQ(reassembled_hash(s, E2_A).GetHex(), GOLD_CBHASH_A); |
| 139 | + EXPECT_EQ(reassembled_hash(s, E2_B).GetHex(), GOLD_CBHASH_B); |
| 140 | +} |
| 141 | + |
| 142 | +// (4) Injectivity: distinct extranonce2 -> distinct reassembled coinbase hash, |
| 143 | +// so the slot the split reserves demonstrably carries the miner's nonce. |
| 144 | +TEST(DashStratumExtranonceSplit, DistinctExtranonce2AreInjective) { |
| 145 | + CoinbSplit s = split_coinb(make_layout()); |
| 146 | + uint256 z = reassembled_hash(s, E2_ZERO); |
| 147 | + uint256 a = reassembled_hash(s, E2_A); |
| 148 | + uint256 b = reassembled_hash(s, E2_B); |
| 149 | + EXPECT_NE(z, a); |
| 150 | + EXPECT_NE(z, b); |
| 151 | + EXPECT_NE(a, b); |
| 152 | +} |
| 153 | + |
| 154 | +// (5) Size is load-bearing: a wrong-width extranonce2 reassembles to a coinbase |
| 155 | +// of the wrong length. Only the advertised EXTRANONCE2_SIZE reproduces 40B. |
| 156 | +TEST(DashStratumExtranonceSplit, WrongWidthExtranonce2BreaksLength) { |
| 157 | + CoinbSplit s = split_coinb(make_layout()); |
| 158 | + const std::vector<unsigned char> e2_short(EXTRANONCE2_SIZE - 1, 0xEE); |
| 159 | + const std::vector<unsigned char> e2_long(EXTRANONCE2_SIZE + 1, 0xEE); |
| 160 | + EXPECT_EQ(reassemble(s, E2_A).size(), kTotal); |
| 161 | + EXPECT_NE(reassemble(s, e2_short).size(), kTotal); |
| 162 | + EXPECT_NE(reassemble(s, e2_long).size(), kTotal); |
| 163 | +} |
| 164 | + |
| 165 | +// (6) Bad offset guard: split_coinb throws when the nonce64 slot runs past the |
| 166 | +// coinbase end (mirrors the in-source runtime_error precondition). |
| 167 | +TEST(DashStratumExtranonceSplit, BadOffsetThrows) { |
| 168 | + CoinbaseLayout lay = make_layout(); |
| 169 | + lay.nonce64_offset = lay.bytes.size(); // slot would start at EOF -> +8 overruns |
| 170 | + EXPECT_THROW(split_coinb(lay), std::runtime_error); |
| 171 | +} |
0 commit comments