Skip to content

Commit c82bf56

Browse files
committed
test(dash): S8 stratum-binding contract KAT (extranonce2 nonce64-slot)
Fenced test-only slice: pins the get_work() -> stratum job BINDING contract (coinbase nonce64 / extranonce2 slot geometry + coinb1/coinb2 split and reassembly that partitions the miner search space) against oracle frstrtr/p2pool-dash @9a0a609 work.py:21,421,436-438. 6 KATs, all derived independently of the SUT (oracle contract + Python sha256d goldens): Extranonce2WidthOraclePin EXTRANONCE2_SIZE == COINBASE_NONCE_LENGTH == 8 SplitBoundaryGeometry coinb1=bytes[:off], coinb2=bytes[off+8:] ZeroNonceIsIdentity zero nonce reassembles gentx byte-for-byte (:437) SlotIsolation only the 8 slot bytes move across extranonce2 BindingInjectivity distinct extranonce2 -> distinct coinbase leaf GoldenLeafHashes independent sha256d byte-parity anchors Pure/socket-free/node-free (synthetic CoinbaseLayout, no dashd, no sharechain). Touches only test/ + build.yml allowlist -> IDENTIFIER-neutral, non-consensus, independently landable ahead of the migration GO. No self-merge.
1 parent 527d342 commit c82bf56

3 files changed

Lines changed: 183 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
test_threading test_weights \
8989
test_header_chain test_mempool test_template_builder \
9090
test_doge_chain test_compact_blocks test_dash_x11_kat \
91-
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_cb_payee test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node \
91+
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_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node \
9292
test_multiaddress_pplns test_pplns_stress \
9393
test_hash_link test_decay_pplns \
9494
test_pplns_consensus \
@@ -238,7 +238,7 @@ jobs:
238238
test_threading test_weights \
239239
test_header_chain test_mempool test_template_builder \
240240
test_doge_chain test_compact_blocks test_dash_x11_kat \
241-
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_cb_payee test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node \
241+
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_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node \
242242
test_hash_link test_decay_pplns \
243243
test_pplns_consensus \
244244
test_v36_script_sorting test_v36_cross_impl_refhash \

test/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,16 @@ if (BUILD_TESTING AND GTest_FOUND)
318318
target_link_libraries(test_dash_work_job_targets PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39): core stratum/web_server TUs
319319
gtest_add_tests(test_dash_work_job_targets "" AUTO)
320320

321+
add_executable(test_dash_stratum_binding test_dash_stratum_binding.cpp)
322+
target_link_libraries(test_dash_stratum_binding PRIVATE
323+
GTest::gtest_main GTest::gtest
324+
dash_x11 core
325+
nlohmann_json::nlohmann_json
326+
${Boost_LIBRARIES}
327+
)
328+
target_link_libraries(test_dash_stratum_binding PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
329+
gtest_add_tests(test_dash_stratum_binding "" AUTO)
330+
321331
# share_init_verify gentx hash_link byte-parity vs oracle (coinbase_payload
322332
# VarStr prefix). Header-only over the dash_x11 + core link set; mirrors the
323333
# test_dash_conformance link set.

test/test_dash_stratum_binding.cpp

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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

Comments
 (0)