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