fix(p2p): consult connection limits before any other behaviour - #640
Conversation
The NetworkBehaviour derive asks each field for a connection handler in declaration order and stops at the first refusal, and a refused connection never produces a ConnectionClosed. connection_limits was the last field, so every request_response::Behaviour in ReqResp had already recorded each connection it went on to refuse. That phantom outlived the peer's real connections: request-response's own debug_assert in on_connection_closed panicked the P2P task of a release-fast Platåberget follower, and without debug assertions a request routed to the phantom is dropped with no OutboundFailure. Upstream tracks this as libp2p/rust-libp2p#4773 (the assert) and #4870 (the ordering trap). #6601 would fix it inside request-response, but it is unreviewed, and putting the limits first is the fix the maintainers advise.
🤖 Kimi Code ReviewI'll review this PR focusing on the libp2p connection ordering fix and its test. This is a well-targeted fix for a subtle libp2p interaction bug. Overall AssessmentThe fix is correct and well-explained. The PR addresses a real bug where Issues Found1. Test uses
|
| Priority | Item | Action |
|---|---|---|
| Medium | Test only checks one ReqResp field |
Check all request-response protocols |
| Low | ConnectionId::new_unchecked usage |
Add safety comment |
| Low | Missing outbound test | Consider adding |
| Low | libp2p derive macro dependency | Add stability warning comment |
| Trivial | Spelling inconsistency | Standardize "behavior/behaviour" |
The core fix is correct and well-justified. The test effectively validates the specific bug scenario, but could be more comprehensive in coverage of ReqResp fields.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Claude Code ReviewReview of PR 640: I read the The fix looks correct.
Suggestions (non-blocking)
No security or consensus concerns in this change. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Codex Code ReviewLooks good overall. The change is narrow, addresses a real libp2p ordering hazard, and the added regression test exercises the exact failure mode.
No consensus-layer logic is touched here, so fork choice / attestation / state transition / SSZ / XMSS risks do not appear impacted by this PR. One small maintainability note:
Validation
If you want, I can also do a second pass on the surrounding Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🗒️ Description / Motivation
A Platåberget follower built with
release-fast(which keepsdebug-assertionson) lost its P2P task to a panic inside libp2p request-response. The process, the API and the chain actor kept running, so the head froze for about five hours with nothing but this in the log:That line is
debug_assert_eq!(connections.is_empty(), remaining_established == 0)inon_connection_closed: request-response still counted a connection to a peer the swarm said had none left.The cause is the field order of
Behaviour, whereconnection_limitscame last:handle_established_*_connectiononBehaviour; the derive calls each field in declaration order with?request_response::BehaviourinReqResprecords the connection in itsconnectedmap (preload_new_handler)connection_limitsrefuses it (per-peer, total, inbound or outbound ceiling)ListenFailure/DialFailure, and never aConnectionEstablishedorConnectionClosedfor it. Request-response ignores both, so the entry staysremaining_established == 0, while request-response still holds the phantom: the assert firesWithout debug assertions the phantom is worse than a leak.
try_send_requestpicks a connection byrequest_id % connections.len(), so some requests to that peer go toNotifyHandler::One(phantom). The swarm drops an event for an unknown connection silently (swarm/src/lib.rs:1214in the pinned fork), and the request timeout lives in the handler that was never spawned, so noOutboundFailureever comes back. ethlambda retires in-flight requests onOutboundFailure.What Changed
crates/net/p2p/src/lib.rs:connection_limitsis now the first field ofBehaviour(and of its struct literal inbuild_swarm), with a doc comment saying why it has to stay first. A refusal now happens before any other behaviour sees the connection.crates/net/p2p/src/lib.rs(tests): a regression test, below.Correctness / Behavior Guarantees
connection_limitsis safe to put first. It records established connections only onFromSwarm::ConnectionEstablished, emits no events (pollis alwaysPending), and its handler isdummy, so the protocols offered and the event dispatch are unchanged.unlimited_connections(), which never refuses, so the lean network is unaffected.Tests Added / Run
tests::a_connection_the_limits_refuse_leaves_no_request_response_statebuilds the real beacon swarm withbuild_swarmand drives itsBehaviourthe way the swarm does: two connections from one peer (MAX_CONNECTIONS_PER_PEER), a third refused and reported as aListenFailure, then both held connections close. Before the fix it fails in both build modes:release-fast, as CI runs it): the production panic atrequest-response/src/lib.rs:708:9CARGO_PROFILE_RELEASE_FAST_DEBUG_ASSERTIONS=false): its own assertion, since the field still reports the peer as connectedAfter the fix it passes in both.
cargo test -p ethlambda-p2p --lib --profile release-fast: 208 passed, 1 ignored.Related Issues / PRs
handle_*callbacks take&mut self), open.ConnectionEstablished. Open and unreviewed; it could be cherry-picked intolambdaclass/rust-libp2plater, and this PR does not depend on it.✅ Verification Checklist
make fmt— clean for this change.cargo fmt --all -- --checkalso flags themodorder inbin/ethlambda/src/main.rs, which is already onbeacon-chain-integration(from c635524) and left out of this PRmake lint(clippy with-D warnings) — cleanmake test(test-consensusplustest-node, atrelease-fast) — left to CI; ran the p2p unit tests above