fix(l1): back off failed dials, pace discovery by saturation and add --p2p.netrestrict - #7257
fix(l1): back off failed dials, pace discovery by saturation and add --p2p.netrestrict#7257ilitteri wants to merge 1 commit into
Conversation
…restrict A node on a small network never left its startup pacing: both the RLPx dialer (100ms) and the discovery lookup tick (500ms, alpha 3, per protocol) eased only on connected/target-peers, and with the default target of 100 a devnet stayed at the fastest rate forever. Dial candidates had no per-node state, so a candidate that never answered was redialed every sweep of the pool, and every lookup in a saturated network converged at once and restarted on the next tick with a fresh target. Behind a carrier-grade NAT that was enough traffic for an operator's ISP to rate-limit them. Pool entries now remember their last dial and consecutive failures and are left alone for 35s doubling per failure, capped at 30 minutes; a successful connection resets the count. Contacts discovery could not reach are skipped by the dialer and evicted from the pool on prune, and the per-sweep already_tried_peers set is gone. The peer table exposes a monotonic discovered_count, and a lookup that finishes without adding to the pool counts as empty: each empty lookup in a row doubles the wait before the next one, up to the steady-state interval, while in-flight lookups keep the fast tick. The RLPx initiator idles at the steady interval when it has nothing to dial. --p2p.netrestrict <CIDR,...> confines all peer traffic to the given networks: discovered nodes outside are never stored, pinged or dialed, bootnodes outside are dropped with a warning, and inbound TCP and UDP from outside is discarded before any handshake. Disabling both discovery protocols now keeps the bootnodes as static dial candidates instead of leaving the node with no peers. The discv4 ping and pong handlers treat a missing record as seq 0 and request the ENR, as the discv5 handler already did, so discv4 contacts get a fork-id verdict without waiting for the random ENR lookup.
|
🤖 Kimi Code ReviewI'll review this PR which adds OverviewThis PR introduces:
Critical Issues1. Race condition in
|
| Priority | Issue | Location | Fix |
|---|---|---|---|
| High | discovered_count overcounts on duplicate inserts |
peer_table.rs:604-605 |
Check insert return value |
| Medium | Discv4State missing explicit Default for new fields |
discv4/server.rs |
Add Default impl or verify derive |
| Medium | Repeated Vec<IpNet> clones |
initializers.rs |
Create NetRestrict once, clone it |
| Low | Inconsistent logging levels for drops | Multiple | Standardize on debug! or warn! |
| Low | pub mod netrestrict redundant with re-export |
`p2p.rs:70 |
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Codex Code Review
Beyond that, the production-side changes look sound. I couldn’t run the Rust tests here because the environment’s Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Lines of code reportTotal lines added: Detailed view |
🤖 Claude Code ReviewThis confirms full coverage. I have enough context to finalize the review. Review:
|
Motivation
An operator running several ethrex containers in a private devnet behind a carrier-grade NAT reported that each container opens ~10 TCP connections/s and ~8 UDP packets/s towards addresses outside the devnet, enough for their ISP to rate-limit them, while the other clients in the same devnet stay quiet. They asked for a way to keep ethrex from crawling anything outside the devnet.
The numbers are ethrex's own startup pacing, and ethrex never leaves startup pacing in a small network:
connected / --p2p.target-peers. With the default target of 100 a devnet stays at the fastest rate forever.Description
35s · 2^(failures-1), capped at 30 minutes (35 s is geth'sdialHistoryExpiration). A successful connection resets the counter. Contacts discovery could not reach (disposable) are skipped by the dialer and evicted from the pool on prune. The per-sweepalready_tried_peersset is gone.discovered_count. A lookup that finishes without adding anything to the pool counts as empty, and each empty lookup in a row doubles the wait before the next one (500 ms → 1 s → 2 s → … → 10 s). In-flight lookups keep the fast tick so they still finish promptly.--p2p.netrestrict <CIDR,...>(envETHREX_P2P_NETRESTRICT), mirroring geth's--netrestrict: nodes discovered outside the list are never stored, pinged or dialed; bootnodes outside are dropped with a warning; inbound TCP and UDP from outside are discarded before any handshake.--p2p.discv4=false --p2p.discv5=falsethe bootnodes are still handed to the dialer, so the node connects only to them.Deliberately left out: dialing only ENR-verified discv4 candidates, the way geth's
AsyncFilter(RequestENR)does. That changes mainnet bootstrap throughput and should be measured on a real node first; the ENR-gap fix above moves verdicts closer without gating dials on them.Docs:
docs/l1/running/startup.mdgains a "Private networks and devnets" section anddocs/CLI.mdis regenerated.How to test:
cargo test -p ethrex-p2p --libcovers the backoff schedule and eligibility, pool eviction of unreachable contacts, netrestrict filtering over both discovery paths,discovered_count, the saturation pacing curve and CIDR matching. To see it live, start two or three ethrex nodes on a local genesis with each other as--bootnodesand watch connection attempts withadmin_peersortcpdump: retries of an unreachable candidate now back off per node instead of recurring every pool sweep, lookups settle at one every 10 s, and with--p2p.netrestrict <devnet CIDR>anything outside is ignored (visible asIgnoring node outside --p2p.netrestrictat trace level andDropping inbound connection from outside --p2p.netrestrictat debug level).Checklist
STORE_SCHEMA_VERSION(crates/storage/lib.rs) if the PR includes breaking changes to theStorerequiring a re-sync. (Not needed: noStorechanges.)