Skip to content

Commit f38b9ca

Browse files
committed
Dash Phase C-PAY: block-replay test harness Day 1 — dumper + 66 fixtures
Phase 1 of the block-replay test harness per frstrtr/the/docs/c2pool-dash-block-replay-test-harness.md. Day 2 will add the test target that consumes these fixtures. The motivation is to shift consensus-correctness validation from soak-driven (days, stochastic) to replay-driven (minutes, deterministic). Bug 15 (DIP-0027 platform reward) sat on-chain for ~7 months because soak-MISMATCH only fires when an MN identity mismatch doesn't fire first; a pure-replay harness would have failed on iteration 1. Changes: - src/impl/dash/coin/rpc.{hpp,cpp}: expose getblockhash(int height) - src/impl/dash/coin/block_dumper.hpp: BlockDumper that walks N heights, writes one JSON fixture per block (idempotent — skips existing files on re-run). Plus canonical_fixture_heights(tip) producing the curated coverage set (V20 boundary, MN_RR boundary, superblocks, halvings, known-Bug anchors, recent tip). - src/impl/dash/main_dash.cpp: --dump-blocks DIR + --dump-blocks-heights CSV one-shot CLI mirroring the --dump-mn-snapshot flow. - test/fixtures/dash_blocks/: 66 deduplicated JSON fixtures (~17 MB) spanning h=1,987,775..2,473,133. Includes the Bug 12/13/14/15 diagnostic anchors. Generated by: c2pool-dash --dump-blocks test/fixtures/dash_blocks \ --dashd-rpc 192.168.86.24:9998:... Each fixture file contains: height, block_hash, block_hex (verbosity=0 raw), block_verbose (verbosity=2 — decoded coinbase + tx details + cb_tx extraPayload fields for ground-truth assertions). Day 2 will add test/test_dash_block_replay.cpp with per-block assertions on platform reward, MN payment, expected payee, SML root, quorums root, credit-pool balance, and subsidy split sum.
1 parent dc9d581 commit f38b9ca

70 files changed

Lines changed: 209646 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#pragma once
2+
3+
/// Block-replay regression test harness — Phase 1 dumper.
4+
///
5+
/// Connects to a trusted dashd via JSON-RPC and dumps N blocks as
6+
/// JSON fixtures to disk. Each fixture file captures everything the
7+
/// replay test needs to assert bit-exact formula correctness:
8+
/// - block hex (verbosity=0 raw)
9+
/// - getblock verbosity=2 JSON (decoded coinbase outputs, tx details,
10+
/// cb_tx extraPayload fields)
11+
/// - height + block hash
12+
///
13+
/// One file per block: `<out_dir>/h<HEIGHT>.json`. Pre-existing files
14+
/// are skipped (idempotent — safe to re-run after RPC outage).
15+
///
16+
/// Design doc: frstrtr/the/docs/c2pool-dash-block-replay-test-harness.md
17+
///
18+
/// Usage from main_dash:
19+
/// dash::coin::BlockDumper dumper{rpc, out_dir, heights};
20+
/// dumper.run(); // walks heights, writes files, logs progress
21+
22+
#include <impl/dash/coin/rpc.hpp>
23+
#include <core/log.hpp>
24+
#include <core/uint256.hpp>
25+
26+
#include <nlohmann/json.hpp>
27+
28+
#include <filesystem>
29+
#include <fstream>
30+
#include <string>
31+
#include <vector>
32+
33+
namespace dash {
34+
namespace coin {
35+
36+
struct BlockDumper {
37+
NodeRPC& rpc;
38+
std::string out_dir;
39+
std::vector<int> heights; // explicit list — caller picks coverage
40+
41+
// Returns number of blocks successfully written. Skips heights that
42+
// already have a file on disk (re-runnable after failure).
43+
int run()
44+
{
45+
std::filesystem::create_directories(out_dir);
46+
47+
int written = 0;
48+
int skipped = 0;
49+
int failed = 0;
50+
for (size_t i = 0; i < heights.size(); ++i) {
51+
int h = heights[i];
52+
std::string path = out_dir + "/h" + std::to_string(h) + ".json";
53+
54+
if (std::filesystem::exists(path)) {
55+
++skipped;
56+
continue;
57+
}
58+
59+
try {
60+
nlohmann::json hash_resp = rpc.getblockhash(h);
61+
if (!hash_resp.is_string()) {
62+
LOG_WARNING << "[BLOCK-DUMP] h=" << h
63+
<< " getblockhash returned non-string";
64+
++failed;
65+
continue;
66+
}
67+
std::string hash_hex = hash_resp.get<std::string>();
68+
uint256 hash;
69+
hash.SetHex(hash_hex);
70+
71+
// Two RPC calls: verbosity=0 gives raw hex (for c2pool's
72+
// parser), verbosity=2 gives the decoded JSON (for ground
73+
// truth: coinbase vouts, cb_tx fields, payee addresses).
74+
nlohmann::json block_hex = rpc.getblock(hash, 0);
75+
nlohmann::json block_verbose = rpc.getblock(hash, 2);
76+
77+
nlohmann::json fixture = {
78+
{"height", h},
79+
{"block_hash", hash_hex},
80+
{"block_hex", block_hex},
81+
{"block_verbose", block_verbose},
82+
};
83+
84+
std::ofstream out(path);
85+
out << fixture.dump(2);
86+
++written;
87+
88+
if ((written + skipped) % 10 == 0 || i + 1 == heights.size()) {
89+
LOG_INFO << "[BLOCK-DUMP] progress: "
90+
<< (i + 1) << "/" << heights.size()
91+
<< " (written=" << written
92+
<< " skipped=" << skipped
93+
<< " failed=" << failed << ")";
94+
}
95+
} catch (const std::exception& e) {
96+
LOG_WARNING << "[BLOCK-DUMP] h=" << h
97+
<< " failed: " << e.what();
98+
++failed;
99+
}
100+
}
101+
102+
LOG_INFO << "[BLOCK-DUMP] complete: written=" << written
103+
<< " skipped=" << skipped
104+
<< " failed=" << failed
105+
<< " out_dir=" << out_dir;
106+
return written;
107+
}
108+
};
109+
110+
/// Produce the canonical 100-block fixture selection from the design doc.
111+
/// Heights chosen to span: V20 boundary, MN_RR boundary, superblocks,
112+
/// halvings, broad recent regression coverage, recent tip.
113+
///
114+
/// `tip_height` should be the current chain tip (from getblockchaininfo)
115+
/// — the "recent tip" portion is anchored to it. Pass `recent_tip=true`
116+
/// to include tip-relative heights; `false` if tip blocks aren't desired
117+
/// (e.g. for offline replay against an older snapshot).
118+
inline std::vector<int> canonical_fixture_heights(int tip_height)
119+
{
120+
std::vector<int> hs;
121+
122+
// 5 blocks straddling V20 activation (h=1,987,776 mainnet)
123+
for (int delta : {-1, 0, 1, 24, 224}) hs.push_back(1'987'776 + delta);
124+
125+
// 5 blocks straddling MN_RR activation (h=2,128,896 — Bug 15 boundary)
126+
for (int delta : {-1, 0, 1, 4, 104}) hs.push_back(2'128'896 + delta);
127+
128+
// 5 superblock heights post-MN_RR (cycle=16,616)
129+
for (int n : {129, 130, 131, 140, 145}) hs.push_back(n * 16'616);
130+
131+
// 5 halving-boundary blocks near recent tip
132+
for (int n : {10, 11, 12}) {
133+
int h = n * 210'240;
134+
if (h < tip_height) {
135+
hs.push_back(h - 1);
136+
hs.push_back(h);
137+
}
138+
}
139+
140+
// 20 random-spread blocks across post-MN_RR mainnet
141+
for (int i = 0; i < 20; ++i) {
142+
hs.push_back(2'200'000 + i * 11'000); // every ~11k blocks
143+
}
144+
145+
// 20 recent-tip blocks (last 20 if tip is set)
146+
if (tip_height > 0) {
147+
for (int i = 20; i > 0; --i) hs.push_back(tip_height - i);
148+
}
149+
150+
// 20 blocks at known interesting heights — Bug discovery anchors
151+
// (Bug 15 caught here, plus padding for future-Bug anchors)
152+
for (int h : {
153+
2'460'249, // first DMN snapshot block (mn_snapshot.hpp pin)
154+
2'463'018, // Bug 12 PoSe-ban height
155+
2'462'994, // Bug 13 CProUpServTx parse failure block
156+
2'465'346, // Bug 14 implicit-revive height
157+
2'465'862, // Bug 14 snapshot pin
158+
2'470'904, // Bug 15 platform-reward verification block
159+
2'470'890, // Bug-15 recovery snapshot block
160+
}) {
161+
hs.push_back(h);
162+
}
163+
164+
// De-dup + sort + drop out-of-range
165+
std::sort(hs.begin(), hs.end());
166+
hs.erase(std::unique(hs.begin(), hs.end()), hs.end());
167+
if (tip_height > 0) {
168+
hs.erase(std::remove_if(hs.begin(), hs.end(),
169+
[tip_height](int h) { return h > tip_height; }),
170+
hs.end());
171+
}
172+
173+
return hs;
174+
}
175+
176+
} // namespace coin
177+
} // namespace dash

src/impl/dash/coin/rpc.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ nlohmann::json NodeRPC::getblock(const uint256& blockhash, int verbosity)
220220
return call_method("getblock", {blockhash, verbosity});
221221
}
222222

223+
nlohmann::json NodeRPC::getblockhash(int height)
224+
{
225+
return call_method("getblockhash", {height});
226+
}
227+
223228
// ── Dash GBT parsing ─────────────────────────────────────────────────────────
224229

225230
void NodeRPC::parse_payments(const nlohmann::json& gbt, DashWorkData& out)

src/impl/dash/coin/rpc.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ class NodeRPC : public jsonrpccxx::IClientConnector
6060
nlohmann::json getmininginfo();
6161
nlohmann::json getblockheader(const uint256& header, bool verbose = true);
6262
nlohmann::json getblock(const uint256& blockhash, int verbosity = 1);
63+
// Maps height → block hash. Used by block_dumper for the replay harness.
64+
nlohmann::json getblockhash(int height);
6365

6466
// Fetch + parse GBT → DashWorkData (Dash-specific field extraction).
6567
DashWorkData getwork();

src/impl/dash/main_dash.cpp

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <impl/dash/coin/mn_state_db.hpp>
3434
#include <impl/dash/coin/mn_snapshot.hpp>
3535
#include <impl/dash/coin/mn_snapshot_rpc.hpp>
36+
#include <impl/dash/coin/block_dumper.hpp>
3637
#include <impl/dash/coin/mn_state_machine.hpp>
3738
#include <impl/dash/coin/mempool.hpp>
3839
#include <impl/dash/coin/subsidy.hpp>
@@ -190,6 +191,8 @@ int main(int argc, char* argv[])
190191
std::string gbt_source = "auto";
191192
std::string dash_mn_snapshot_path; // --dash-mn-snapshot operator override
192193
std::string dump_mn_snapshot_output; // --dump-mn-snapshot one-shot dumper
194+
std::string dump_blocks_out_dir; // --dump-blocks one-shot fixture dump
195+
std::string dump_blocks_heights; // optional CSV; empty → canonical 100
193196
uint16_t stratum_port = 0; // 0 = disabled; canonical Dash p2pool stratum port is 7903
194197
std::string mining_address; // required when stratum+rpc are wired
195198
double share_difficulty_default = 0.001; // vardiff lands later
@@ -241,6 +244,12 @@ int main(int argc, char* argv[])
241244
else if (arg == "--dump-mn-snapshot" && i + 1 < argc) {
242245
dump_mn_snapshot_output = argv[++i];
243246
}
247+
else if (arg == "--dump-blocks" && i + 1 < argc) {
248+
dump_blocks_out_dir = argv[++i];
249+
}
250+
else if (arg == "--dump-blocks-heights" && i + 1 < argc) {
251+
dump_blocks_heights = argv[++i];
252+
}
244253
else if (arg == "--dashd" && i + 1 < argc) {
245254
std::string addr = argv[++i];
246255
auto colon = addr.find(':');
@@ -473,6 +482,85 @@ int main(int argc, char* argv[])
473482
return exit_code;
474483
}
475484

485+
// ── --dump-blocks one-shot path (block-replay test harness, Phase 1) ──
486+
// Walks N heights via dashd RPC, writes one JSON fixture per block to
487+
// out_dir. Heights either explicit via --dump-blocks-heights "H1,H2,..."
488+
// or the canonical 100-block selection from block_dumper.hpp.
489+
//
490+
// Usage:
491+
// c2pool-dash --dump-blocks test/fixtures/dash_blocks/ \
492+
// --dashd-rpc HOST:PORT:USER:PASS
493+
// c2pool-dash --dump-blocks test/fixtures/dash_blocks/ \
494+
// --dump-blocks-heights 2470904,2470905 \
495+
// --dashd-rpc HOST:PORT:USER:PASS
496+
//
497+
// Design doc: frstrtr/the/docs/c2pool-dash-block-replay-test-harness.md
498+
if (!dump_blocks_out_dir.empty()) {
499+
if (dashd_rpc_host.empty()) {
500+
std::cerr << "[--dump-blocks] requires --dashd-rpc to be set"
501+
<< std::endl;
502+
return 1;
503+
}
504+
boost::asio::io_context dump_ioc;
505+
dash::interfaces::Node dump_iface;
506+
auto dump_rpc = std::make_unique<dash::coin::NodeRPC>(
507+
&dump_ioc, &dump_iface, testnet);
508+
boost::asio::post(dump_ioc, [&]() {
509+
NetService rpc_addr(dashd_rpc_host + ":" + std::to_string(dashd_rpc_port));
510+
dump_rpc->connect(rpc_addr, dashd_rpc_userpass);
511+
});
512+
int exit_code = 0;
513+
auto dump_timer = std::make_shared<boost::asio::steady_timer>(
514+
dump_ioc, std::chrono::seconds(5));
515+
dump_timer->async_wait(
516+
[&, dump_timer](const boost::system::error_code& ec) {
517+
if (ec) return;
518+
if (!dump_rpc || !dump_rpc->is_connected()) {
519+
std::cerr << "[--dump-blocks] RPC not connected after 5s — aborting"
520+
<< std::endl;
521+
exit_code = 2;
522+
dump_ioc.stop();
523+
return;
524+
}
525+
526+
// Build heights list
527+
std::vector<int> heights;
528+
if (!dump_blocks_heights.empty()) {
529+
std::string s = dump_blocks_heights;
530+
size_t pos = 0;
531+
while (pos < s.size()) {
532+
size_t comma = s.find(',', pos);
533+
std::string tok = s.substr(pos, comma - pos);
534+
if (!tok.empty()) heights.push_back(std::stoi(tok));
535+
if (comma == std::string::npos) break;
536+
pos = comma + 1;
537+
}
538+
} else {
539+
int tip = 0;
540+
try {
541+
auto info = dump_rpc->getblockchaininfo();
542+
tip = info.value("blocks", 0);
543+
} catch (...) {
544+
std::cerr << "[--dump-blocks] getblockchaininfo failed" << std::endl;
545+
exit_code = 3;
546+
dump_ioc.stop();
547+
return;
548+
}
549+
heights = dash::coin::canonical_fixture_heights(tip);
550+
std::cout << "[--dump-blocks] using canonical fixture set: "
551+
<< heights.size() << " heights, tip=" << tip << std::endl;
552+
}
553+
554+
dash::coin::BlockDumper dumper{*dump_rpc, dump_blocks_out_dir, std::move(heights)};
555+
int written = dumper.run();
556+
std::cout << "[--dump-blocks] OK — wrote " << written
557+
<< " new fixture(s) to " << dump_blocks_out_dir << std::endl;
558+
dump_ioc.stop();
559+
});
560+
dump_ioc.run();
561+
return exit_code;
562+
}
563+
476564
// X11 self-test
477565
{
478566
unsigned char zeros[80] = {};

0 commit comments

Comments
 (0)