@@ -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
0 commit comments