|
| 1 | +// DASH S8 STRATUM-BINDING contract KAT. |
| 2 | +// |
| 3 | +// Pins the get_work() -> stratum job binding contract: the extranonce2 |
| 4 | +// (nonce64) coinbase slot geometry and the coinb1/coinb2 split/reassembly |
| 5 | +// that partitions the miner search space. Where test_dash_work_job_targets |
| 6 | +// pins the job-TARGET arithmetic (work.py:368-426), this pins the coinbase |
| 7 | +// NONCE-BINDING half of the same get_work() path (work.py:21,421,436-438). |
| 8 | +// |
| 9 | +// ORACLE: frstrtr/p2pool-dash @9a0a609 p2pool/work.py |
| 10 | +// :21 COINBASE_NONCE_LENGTH = 8 |
| 11 | +// :421 coinb1 = packed_gentx[:-payload - COINBASE_NONCE_LENGTH - 4] |
| 12 | +// :436 assert len(coinbase_nonce) == COINBASE_NONCE_LENGTH |
| 13 | +// :437 new_gentx = coinb1 + coinbase_nonce + packed_gentx[-payload-4:] |
| 14 | +// ... == packed_gentx iff coinbase_nonce == '\0'*8 |
| 15 | +// Every expected value below is derived from the oracle contract (or an |
| 16 | +// independent Python sha256d, see each golden), NOT from the SUT: a true |
| 17 | +// byte-parity pin, not a tautology. |
| 18 | +// |
| 19 | +// Pure / socket-free / node-free: no VM200/201 dashd, no live sharechain, |
| 20 | +// no template build -- the contract is exercised on a synthetic CoinbaseLayout |
| 21 | +// so the geometry/binding invariants stand alone from generate_transaction. |
| 22 | + |
| 23 | +#include <gtest/gtest.h> |
| 24 | + |
| 25 | +#include <cstddef> |
| 26 | +#include <numeric> |
| 27 | +#include <span> |
| 28 | +#include <string> |
| 29 | +#include <vector> |
| 30 | + |
| 31 | +#include <impl/dash/coinbase_builder.hpp> // CoinbaseLayout, split_coinb, sha256d, EXTRANONCE2_SIZE |
| 32 | +#include <btclibs/util/strencodings.h> // HexStr |
| 33 | +#include <core/uint256.hpp> |
| 34 | + |
| 35 | +using dash::coinbase::CoinbaseLayout; |
| 36 | +using dash::coinbase::CoinbSplit; |
| 37 | +using dash::coinbase::split_coinb; |
| 38 | +using dash::coinbase::sha256d; |
| 39 | +using dash::coinbase::EXTRANONCE2_SIZE; |
| 40 | + |
| 41 | +namespace { |
| 42 | + |
| 43 | +// Synthetic 40-byte coinbase: bytes[i] = i, with the 8-byte nonce64 slot at |
| 44 | +// [28,36) zeroed (nonce64_offset = 40 - locktime(4) - nonce64(8) = 28), mirroring |
| 45 | +// the real coinbase tail [... nonce64 8B][locktime 4B] for a type-0 tx. |
| 46 | +constexpr size_t kTotal = 40; |
| 47 | +constexpr size_t kOffset = 28; // = kTotal - 4 - 8 |
| 48 | + |
| 49 | +CoinbaseLayout make_layout() |
| 50 | +{ |
| 51 | + CoinbaseLayout lay; |
| 52 | + lay.bytes.resize(kTotal); |
| 53 | + std::iota(lay.bytes.begin(), lay.bytes.end(), static_cast<unsigned char>(0)); |
| 54 | + for (size_t i = kOffset; i < kOffset + EXTRANONCE2_SIZE; ++i) |
| 55 | + lay.bytes[i] = 0x00; |
| 56 | + lay.nonce64_offset = kOffset; |
| 57 | + lay.ref_hash_offset = kOffset - 32; // not exercised here; kept consistent |
| 58 | + return lay; |
| 59 | +} |
| 60 | + |
| 61 | +// Reassemble coinb1 || extranonce2 || coinb2 as a stratum miner does, then |
| 62 | +// sha256d it -- the coinbase-hash leaf the miner feeds the merkle root. |
| 63 | +std::vector<unsigned char> reassemble(const CoinbaseLayout& lay, |
| 64 | + std::span<const unsigned char> e2) |
| 65 | +{ |
| 66 | + std::vector<unsigned char> cb; |
| 67 | + cb.reserve(lay.bytes.size()); |
| 68 | + cb.insert(cb.end(), lay.bytes.begin(), lay.bytes.begin() + lay.nonce64_offset); |
| 69 | + cb.insert(cb.end(), e2.begin(), e2.end()); |
| 70 | + cb.insert(cb.end(), lay.bytes.begin() + lay.nonce64_offset + EXTRANONCE2_SIZE, |
| 71 | + lay.bytes.end()); |
| 72 | + return cb; |
| 73 | +} |
| 74 | + |
| 75 | +uint256 hash_of(const std::vector<unsigned char>& b) |
| 76 | +{ |
| 77 | + return sha256d(std::span<const unsigned char>(b.data(), b.size())); |
| 78 | +} |
| 79 | + |
| 80 | +const unsigned char E2_A[8] = {1, 2, 3, 4, 5, 6, 7, 8}; |
| 81 | +const unsigned char E2_B[8] = {0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xf0, 0x0d}; |
| 82 | +const unsigned char E2_ZERO[8] = {0, 0, 0, 0, 0, 0, 0, 0}; |
| 83 | + |
| 84 | +} // namespace |
| 85 | + |
| 86 | +// (1) Extranonce2 width == oracle COINBASE_NONCE_LENGTH (work.py:21). A width |
| 87 | +// mismatch silently corrupts every miner submission, so pin it hard. |
| 88 | +TEST(DashStratumBinding, Extranonce2WidthOraclePin) |
| 89 | +{ |
| 90 | + EXPECT_EQ(EXTRANONCE2_SIZE, static_cast<size_t>(8)); |
| 91 | +} |
| 92 | + |
| 93 | +// (2) Split boundary geometry (work.py:421). coinb1 is exactly bytes[:offset] |
| 94 | +// and coinb2 is exactly bytes[offset+8:]. Lengths pinned from the layout, |
| 95 | +// independent of the SUT's own hex. |
| 96 | +TEST(DashStratumBinding, SplitBoundaryGeometry) |
| 97 | +{ |
| 98 | + CoinbaseLayout lay = make_layout(); |
| 99 | + CoinbSplit s = split_coinb(lay); |
| 100 | + EXPECT_EQ(s.coinb1_hex.size(), kOffset * 2); // hex chars |
| 101 | + EXPECT_EQ(s.coinb2_hex.size(), (kTotal - kOffset - EXTRANONCE2_SIZE) * 2); |
| 102 | + // coinb1 == HexStr(bytes[:offset]); coinb2 == HexStr(bytes[offset+8:]). |
| 103 | + std::span<const unsigned char> b1(lay.bytes.data(), kOffset); |
| 104 | + std::span<const unsigned char> b2(lay.bytes.data() + kOffset + EXTRANONCE2_SIZE, |
| 105 | + kTotal - kOffset - EXTRANONCE2_SIZE); |
| 106 | + EXPECT_EQ(s.coinb1_hex, HexStr(b1)); |
| 107 | + EXPECT_EQ(s.coinb2_hex, HexStr(b2)); |
| 108 | +} |
| 109 | + |
| 110 | +// (3) Identity: reassembling coinb1 || '\0'*8 || coinb2 reproduces the coinbase |
| 111 | +// bytes EXACTLY (work.py:437 -- coinbase_nonce == '\0'*8 => new_gentx == gentx). |
| 112 | +// The zero nonce is the identity element of the binding. |
| 113 | +TEST(DashStratumBinding, ZeroNonceIsIdentity) |
| 114 | +{ |
| 115 | + CoinbaseLayout lay = make_layout(); |
| 116 | + CoinbSplit s = split_coinb(lay); |
| 117 | + std::span<const unsigned char> z(E2_ZERO, EXTRANONCE2_SIZE); |
| 118 | + EXPECT_EQ(s.coinb1_hex + HexStr(z) + s.coinb2_hex, HexStr(lay.bytes)); |
| 119 | + // ...and the reassembled bytes equal the original coinbase byte-for-byte. |
| 120 | + EXPECT_EQ(reassemble(lay, z), lay.bytes); |
| 121 | +} |
| 122 | + |
| 123 | +// (4) Slot isolation: across distinct extranonce2 values, coinb1 and coinb2 are |
| 124 | +// invariant and ONLY the 8 slot bytes move -- the reason a miner fetches |
| 125 | +// coinb1/coinb2 once per job and iterates just the slot. |
| 126 | +TEST(DashStratumBinding, SlotIsolation) |
| 127 | +{ |
| 128 | + CoinbaseLayout lay = make_layout(); |
| 129 | + auto a = reassemble(lay, std::span<const unsigned char>(E2_A, 8)); |
| 130 | + auto b = reassemble(lay, std::span<const unsigned char>(E2_B, 8)); |
| 131 | + ASSERT_EQ(a.size(), lay.bytes.size()); |
| 132 | + ASSERT_EQ(b.size(), lay.bytes.size()); |
| 133 | + for (size_t i = 0; i < a.size(); ++i) { |
| 134 | + if (i >= kOffset && i < kOffset + EXTRANONCE2_SIZE) continue; // slot: may differ |
| 135 | + EXPECT_EQ(a[i], b[i]) << "byte " << i << " outside slot must be invariant"; |
| 136 | + } |
| 137 | + // slot region carries the supplied extranonce2 verbatim. |
| 138 | + for (size_t i = 0; i < EXTRANONCE2_SIZE; ++i) { |
| 139 | + EXPECT_EQ(a[kOffset + i], E2_A[i]); |
| 140 | + EXPECT_EQ(b[kOffset + i], E2_B[i]); |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +// (5) Binding injectivity: distinct extranonce2 -> distinct coinbase hash, and |
| 145 | +// each differs from the zero-slot leaf. This is the search-space PARTITION |
| 146 | +// guarantee -- overlapping leaves would let two miners claim one solution. |
| 147 | +TEST(DashStratumBinding, BindingInjectivity) |
| 148 | +{ |
| 149 | + CoinbaseLayout lay = make_layout(); |
| 150 | + uint256 h0 = hash_of(reassemble(lay, std::span<const unsigned char>(E2_ZERO, 8))); |
| 151 | + uint256 hA = hash_of(reassemble(lay, std::span<const unsigned char>(E2_A, 8))); |
| 152 | + uint256 hB = hash_of(reassemble(lay, std::span<const unsigned char>(E2_B, 8))); |
| 153 | + EXPECT_NE(hA, hB); |
| 154 | + EXPECT_NE(hA, h0); |
| 155 | + EXPECT_NE(hB, h0); |
| 156 | +} |
| 157 | + |
| 158 | +// (6) Golden sha256d byte-parity anchors. Expected values computed independently |
| 159 | +// in Python (hashlib sha256d, big-endian display), NOT from the SUT: |
| 160 | +// base = bytes(range(40)); base[28:36] = 0 |
| 161 | +// zero/A/B insert '\0'*8 / 01..08 / deadbeefcafef00d at [28:36) |
| 162 | +TEST(DashStratumBinding, GoldenLeafHashes) |
| 163 | +{ |
| 164 | + CoinbaseLayout lay = make_layout(); |
| 165 | + EXPECT_EQ(hash_of(reassemble(lay, std::span<const unsigned char>(E2_ZERO, 8))).GetHex(), |
| 166 | + "b0d22de8e7f6765f9d8c289ecd77ad8e0ab6a88c2a4ccf594c509d7775bb54ab"); |
| 167 | + EXPECT_EQ(hash_of(reassemble(lay, std::span<const unsigned char>(E2_A, 8))).GetHex(), |
| 168 | + "9bdc1a3b7b8600535e99c5298b6cb50fde48bd1b20a1ad6883abfbb2799b1b0e"); |
| 169 | + EXPECT_EQ(hash_of(reassemble(lay, std::span<const unsigned char>(E2_B, 8))).GetHex(), |
| 170 | + "a0acea5d125dd3452a24dd84c0d1245450dd237cfe0bc0170d4381195e5f8061"); |
| 171 | +} |
0 commit comments