Skip to content

Commit b7dda42

Browse files
committed
bch(M5): --with-peer-verify — live VM300 proof of won-block P2P leg + forced-stall reissue
Closes the end-to-end gap PR #231 (production run() arming the embedded P2P leg via maybe_start_p2p) left open: its offline step7 test only proves the no-peer RPC-only contract. This adds a co-located, strictly read-only WITH-peer mode against the live VM300 bchn-bch peer proving the two positives: (a) arm_p2p_no_rpc() = run() minus init_rpc drives the REAL maybe_start_p2p() through its configured-peer gate; broadcast_route() reports p2p -> the won-block dispatcher SELECTS the embedded P2P relay leg, not the RPC-only fallback. Routing asserted via a dry sink-selection read (broadcast_route), no block relayed onto mainnet. (b) a forced download-window stall (one unservable hash enqueued through request_block_downloads, the connector deep-reorg re-request sink) makes reissue_count() go nonzero off the LIVE window after BLOCK_DL_TIMEOUT, not the synthetic tick the unit test uses. Live result @VM300 192.168.86.110:8333 (BCHN 29.0.0, start_height 956031, compact-blocks v1): armed=yes route_p2p=yes reissue 0->1 at t=70s -> ALL PASS. Fenced src/impl/bch/ + binary entrypoint only; transport/local block-dl wiring, no PoW/share/coinbase/PPLNS change. p2pool-merged-v36 surface: NONE.
1 parent 4a56e79 commit b7dda42

2 files changed

Lines changed: 159 additions & 0 deletions

File tree

src/c2pool/main_bch.cpp

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ void print_banner(const char* argv0)
6969
<< "c2pool-bch " << C2POOL_VERSION << " — Bitcoin Cash (SHA256d, V36)\n\n"
7070
<< "Usage: " << argv0 << " [--version] [--help] [--selftest]\n"
7171
<< " " << argv0 << " --ibd [--testnet] [--near-tip] [--peer HOST:PORT] [--max-seconds N]\n"
72+
<< " " << argv0 << " --with-peer-verify [--testnet] [--peer HOST:PORT] [--max-seconds N]\n"
7273
<< " " << argv0 << " --leg-c-capture [--rpc-conf PATH]\n"
7374
<< " " << argv0 << " --leg-c-capture-p2p [--rpc-conf PATH] [--p2p-port N]\n\n"
7475
<< "Status: M5 pool/sharechain + embedded-daemon assembly live.\n"
@@ -207,6 +208,122 @@ int run_ibd(const std::string& host, uint16_t port, bool testnet, uint32_t max_s
207208
return 0;
208209
}
209210

211+
// ---------------------------------------------------------------------------
212+
// --with-peer-verify: WITH-PEER end-to-end check of the #231 production arming.
213+
//
214+
// #231 wired the embedded P2P leg into the PRODUCTION run() (maybe_start_p2p),
215+
// closing the won-block RPC-only degradation. Its unit slice only proved the
216+
// OFFLINE no-peer contract; the leg actually FIRING with a configured peer was
217+
// unverified end-to-end (integrator UID 1560). This mode closes that gap
218+
// against the live VM300 bchn-bch peer, strictly READ-ONLY (start_p2p connects
219+
// + getheaders + getdata only -- no init_rpc, no qm/control op):
220+
//
221+
// (a) drives the REAL maybe_start_p2p() through arm_p2p_no_rpc() (run() minus
222+
// init_rpc) and asserts it returned true AND broadcast_route()=="p2p" --
223+
// the won-block dispatcher now TAKES the embedded P2P relay leg, not the
224+
// RPC-only fallback. Routing is asserted WITHOUT relaying a block onto
225+
// mainnet (broadcast_route is a dry sink-selection read); actual
226+
// fire+ACCEPT of a c2pool-built block is the CLOSED co-located-regtest
227+
// broadcaster gate (leg-C), not repeated against live mainnet here.
228+
// (b) forces a REAL download-window stall: enqueues an unservable block hash
229+
// through request_block_downloads -- the EXACT sink the BlockConnector
230+
// deep-reorg re-request wires to (set_block_requester ->
231+
// p2p->request_block_downloads). VM300 never delivers it, so after
232+
// BLOCK_DL_TIMEOUT_SEC the expire tick requeues it and ibd_reissue_count()
233+
// goes NONZERO off the live window -- not the synthetic-tick substitute
234+
// the unit test uses.
235+
//
236+
// p2pool-merged-v36 surface: NONE (transport + local block-dl plumbing only).
237+
// ---------------------------------------------------------------------------
238+
int run_with_peer_verify(const std::string& host, uint16_t port, bool testnet,
239+
uint32_t max_seconds)
240+
{
241+
boost::asio::io_context ctx;
242+
243+
bch::Config cfg("bch-with-peer-verify");
244+
cfg.m_testnet = testnet;
245+
const NetService peer(host, port);
246+
cfg.coin()->m_p2p.address = peer; // the production "configured peer"
247+
cfg.coin()->m_p2p.prefix = testnet
248+
? std::vector<std::byte>{ std::byte{0xf4}, std::byte{0xe5}, std::byte{0xf3}, std::byte{0xf4} }
249+
: std::vector<std::byte>{ std::byte{0xe3}, std::byte{0xe1}, std::byte{0xf3}, std::byte{0xe8} };
250+
251+
EmbeddedDaemon<bch::Config> daemon(&ctx, &cfg, /*anchor_height=*/0);
252+
253+
// (a) Production arming: run() minus init_rpc, driving the real
254+
// maybe_start_p2p() through its configured-peer gate.
255+
const bool armed = daemon.arm_p2p_no_rpc();
256+
const std::string route0 = daemon.broadcast_route();
257+
std::cout << "[verify] arm_p2p_no_rpc -> " << (armed ? "P2P-LIVE" : "RPC-ONLY")
258+
<< "; broadcast_route=" << route0
259+
<< " (peer " << host << ":" << port
260+
<< (testnet ? " testnet" : " mainnet") << ")\n";
261+
262+
int failures = 0;
263+
if (!armed) { std::cerr << "FAIL: maybe_start_p2p did not arm with a configured peer\n"; ++failures; }
264+
265+
bool kicked = false;
266+
bool stall_injected = false;
267+
bool route_p2p_seen = false;
268+
uint32_t elapsed = 0;
269+
const uint32_t TICK = 5;
270+
271+
// An unservable (orphan) block hash: VM300 has no such block, so a getdata
272+
// for it is never satisfied -> the window must time out and reissue it.
273+
uint256 orphan; orphan.SetHex(
274+
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
275+
276+
core::Timer tick(&ctx, /*repeat=*/true);
277+
tick.start(TICK, [&]() {
278+
elapsed += TICK;
279+
280+
if (!kicked && daemon.ibd_handshake_ready()) {
281+
kicked = true;
282+
// Handshake up => P2P transport live => dispatcher takes the P2P leg.
283+
const std::string route = daemon.broadcast_route();
284+
route_p2p_seen = (route == "p2p");
285+
std::cout << "[verify] handshake up; broadcast_route=" << route
286+
<< " (expect p2p) -- won-block dispatcher takes P2P leg\n";
287+
// (b) Inject the forced stall through the connector's re-request sink.
288+
if (auto* p2p = daemon.node().p2p()) {
289+
p2p->request_block_downloads({orphan});
290+
stall_injected = true;
291+
std::cout << "[verify] forced stall: enqueued 1 unservable block via"
292+
<< " request_block_downloads (connector re-request sink);"
293+
<< " awaiting timeout->reissue (BLOCK_DL_TIMEOUT 60s)\n";
294+
}
295+
}
296+
297+
const std::size_t reissue = daemon.ibd_reissue_count();
298+
std::cout << "[verify] t=" << elapsed << "s handshake=" << (kicked ? "up" : "down")
299+
<< " route=" << daemon.broadcast_route()
300+
<< " in_flight=" << daemon.ibd_in_flight()
301+
<< " reissue=" << reissue << "\n";
302+
303+
const bool reissue_seen = stall_injected && reissue > 0;
304+
if (reissue_seen || elapsed >= max_seconds) {
305+
std::cout << "[verify] " << (reissue_seen ? "REISSUE-CONFIRMED" : "DEADLINE")
306+
<< " -- armed=" << (armed ? "yes" : "NO")
307+
<< " route_p2p=" << (route_p2p_seen ? "yes" : "NO")
308+
<< " reissue=" << reissue << "\n";
309+
tick.stop();
310+
ctx.stop();
311+
}
312+
});
313+
314+
ctx.run();
315+
316+
if (!route_p2p_seen) { std::cerr << "FAIL: broadcast_route never == p2p after handshake\n"; ++failures; }
317+
if (daemon.ibd_reissue_count() == 0) { std::cerr << "FAIL: reissue_count stayed 0 (no live stall recovery)\n"; ++failures; }
318+
319+
if (failures == 0) {
320+
std::cout << "with_peer_verify: ALL PASS (P2P leg armed+selected; live reissue confirmed)\n";
321+
return 0;
322+
}
323+
std::cerr << "with_peer_verify: " << failures << " FAILURE(S)\n";
324+
return 1;
325+
}
326+
210327
// ---------------------------------------------------------------------------
211328
// leg-C: dual-path broadcaster capture (RPC leg).
212329
//
@@ -458,6 +575,7 @@ int main(int argc, char** argv)
458575
bool want_ibd = false;
459576
bool want_leg_c = false;
460577
bool want_leg_c_p2p = false;
578+
bool want_with_peer_verify = false;
461579
uint16_t leg_c_p2p_port = 18444; // BCHN regtest P2P default
462580
std::string rpc_conf;
463581
bool testnet = false;
@@ -473,6 +591,7 @@ int main(int argc, char** argv)
473591
}
474592
if (std::strcmp(argv[i], "--help") == 0) want_help = true;
475593
if (std::strcmp(argv[i], "--ibd") == 0) want_ibd = true;
594+
if (std::strcmp(argv[i], "--with-peer-verify") == 0) want_with_peer_verify = true;
476595
if (std::strcmp(argv[i], "--leg-c-capture") == 0) want_leg_c = true;
477596
if (std::strcmp(argv[i], "--leg-c-capture-p2p") == 0) want_leg_c_p2p = true;
478597
if (std::strcmp(argv[i], "--p2p-port") == 0 && i + 1 < argc)
@@ -514,6 +633,9 @@ int main(int argc, char** argv)
514633
return run_leg_c_capture(rpc_conf);
515634
}
516635

636+
if (want_with_peer_verify)
637+
return run_with_peer_verify(host, port, testnet, max_seconds);
638+
517639
if (want_ibd)
518640
return run_ibd(host, port, testnet, max_seconds, near_tip);
519641

src/impl/bch/coin/embedded_daemon.hpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,28 @@ class EmbeddedDaemon {
258258
return true;
259259
}
260260

261+
/// PRODUCTION-FIDELITY read-only arming for the --with-peer-verify harness.
262+
/// Runs run()'s EXACT bring-up sequence MINUS m_node.run() (init_rpc): the
263+
/// external BCHN-RPC fallback is deliberately NOT brought up so the harness
264+
/// exercises ONLY the embedded P2P leg #231 wired, and drives the REAL
265+
/// maybe_start_p2p() through its configured-peer gate -- the method under
266+
/// test. Returns what maybe_start_p2p() returned (true => the won-block P2P
267+
/// relay leg + the BlockConnector deep-reorg re-request sink are both LIVE).
268+
/// After this returns true broadcast_route() reports "p2p": the won-block
269+
/// dispatcher now takes the embedded P2P relay, not the RPC-only
270+
/// degradation #231 closed. Strictly read-only vs VM300 (start_p2p connects
271+
/// + getheaders; no qm/control op, no init_rpc). The caller must have set
272+
/// the configured peer (config->coin()->m_p2p.address) BEFORE calling, just
273+
/// as a YAML load would for the production run(). p2pool-merged-v36 surface:
274+
/// NONE (transport wiring only -- no PoW/share/coinbase/PPLNS change).
275+
bool arm_p2p_no_rpc() {
276+
m_chain.init(); // genesis/checkpoint origin (network-free)
277+
assemble(); // ABLA loop + CoinNode seam + connector sink
278+
wire_chain_ingest(); // new_headers --> HeaderChain (height advance)
279+
pin_cold_start_anchor(); // operator-approved floor-equivalent anchor
280+
return maybe_start_p2p(); // the #231 method under test (configured-peer gate)
281+
}
282+
261283
/// Drive the authoritative HeaderChain from the live P2P header stream.
262284
/// The P2P front-end (NodeP2P) parses `headers` messages and fires
263285
/// new_headers; until this subscription existed NOTHING advanced m_chain
@@ -450,6 +472,21 @@ class EmbeddedDaemon {
450472
return r;
451473
}
452474

475+
/// Read-only routing decision mirroring broadcast_won_block's sink-selection
476+
/// guards WITHOUT transmitting: reports which leg a won block WOULD take
477+
/// given the currently-armed sinks -- "p2p" once the embedded P2P transport
478+
/// is live (maybe_start_p2p armed it), "rpc" if only the external BCHN-RPC
479+
/// fallback is up, "none" offline. Lets the --with-peer-verify harness
480+
/// assert the P2P leg is SELECTED post-arm WITHOUT relaying a block onto the
481+
/// live network, so VM300 stays strictly read-only. Selection order matches
482+
/// broadcast_won_block exactly: embedded P2P is PRIMARY (checked first), the
483+
/// external BCHN-RPC submitblock is the FALLBACK.
484+
const char* broadcast_route() {
485+
if (m_node.has_p2p()) return "p2p";
486+
if (m_coin_node && m_coin_node->has_rpc()) return "rpc";
487+
return "none";
488+
}
489+
453490
private:
454491
config_t* m_config; // not owned (binary entrypoint owns it)
455492
HeaderChain m_chain; // before m_embedded + m_abla: their refs bind here

0 commit comments

Comments
 (0)