Skip to content

Commit 4747b69

Browse files
committed
test(dash/s8): stratum extranonce coinb1/coinb2 split contract KAT
Fenced KAT (test/ + build.yml allowlist only) binding the REAL landed producer dash::coinbase::split_coinb() -- the mining.subscribe/notify wire split coinb1 || <extranonce2> || coinb2 that the get_work slot KAT and notify-roundtrip KAT do not cover. Pins: the split leaves a gap of exactly EXTRANONCE2_SIZE (==8) between coinb1 and coinb2 (== oracle stratum.py COINBASE_NONCE_LENGTH advertised in mining.subscribe); coinb1/coinb2 are the exact bytes around the slot; reassembling coinb1||extranonce2||coinb2 reproduces the EXACT coinbase sha256d the slot/notify KATs golden-anchor; distinct extranonce2 are injective; a wrong-width extranonce2 breaks the coinbase length; bad offset throws. Goldens = independent Python hashlib sha256d (non-circular). Non-consensus, socket-free, node-free. 6/6.
1 parent ff20c36 commit 4747b69

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 test_dash_share_messages \
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 test_dash_share_messages test_dash_stratum_extranonce_split \
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 test_dash_share_messages \
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 test_dash_share_messages test_dash_stratum_extranonce_split \
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
@@ -696,6 +696,16 @@ if (BUILD_TESTING AND GTest_FOUND)
696696
target_link_libraries(test_dash_cb_payee PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
697697
gtest_add_tests(test_dash_cb_payee "" AUTO)
698698

699+
add_executable(test_dash_stratum_extranonce_split test_dash_stratum_extranonce_split.cpp)
700+
target_link_libraries(test_dash_stratum_extranonce_split PRIVATE
701+
GTest::gtest_main GTest::gtest
702+
dash_x11 core
703+
nlohmann_json::nlohmann_json
704+
${Boost_LIBRARIES}
705+
)
706+
target_link_libraries(test_dash_stratum_extranonce_split PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
707+
gtest_add_tests(test_dash_stratum_extranonce_split "" AUTO)
708+
699709
add_executable(test_compact_blocks test_compact_blocks.cpp)
700710
target_link_libraries(test_compact_blocks PRIVATE
701711
GTest::gtest_main GTest::gtest
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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

Comments
 (0)