diff --git a/.changelog/nice-trees-drink.md b/.changelog/nice-trees-drink.md new file mode 100644 index 0000000000..579d202e64 --- /dev/null +++ b/.changelog/nice-trees-drink.md @@ -0,0 +1,9 @@ +--- +reth-network-api: minor +reth-network-types: minor +reth-network: minor +reth-node-core: minor +reth: minor +--- + +Added optional ENR fork ID enforcement to filter out peers from incompatible networks during peer discovery, controlled by the `--enforce-enr-fork-id` CLI flag. diff --git a/crates/net/network-api/src/events.rs b/crates/net/network-api/src/events.rs index 44cd07aebb..d0cf95a8b7 100644 --- a/crates/net/network-api/src/events.rs +++ b/crates/net/network-api/src/events.rs @@ -8,7 +8,7 @@ use reth_eth_wire_types::{ }; use reth_ethereum_forks::ForkId; use reth_network_p2p::error::{RequestError, RequestResult}; -use reth_network_peers::PeerId; +use reth_network_peers::{NodeRecord, PeerId}; use reth_network_types::{PeerAddr, PeerKind}; use reth_tokio_util::EventStream; use std::{ @@ -152,8 +152,13 @@ pub trait NetworkEventListenerProvider: NetworkPeersEvents { pub enum DiscoveryEvent { /// Discovered a node NewNode(DiscoveredEvent), - /// Retrieved a [`ForkId`] from the peer via ENR request, See - EnrForkId(PeerId, ForkId), + /// Retrieved a [`ForkId`] from the peer via ENR request. + /// + /// Contains the full [`NodeRecord`] (peer ID + address) and the reported [`ForkId`]. + /// Used to verify fork compatibility before admitting the peer. + /// + /// See also + EnrForkId(NodeRecord, ForkId), } /// Represents events related to peer discovery in the network. diff --git a/crates/net/network-types/src/peers/config.rs b/crates/net/network-types/src/peers/config.rs index 29e4499b40..cb32781668 100644 --- a/crates/net/network-types/src/peers/config.rs +++ b/crates/net/network-types/src/peers/config.rs @@ -172,6 +172,11 @@ pub struct PeersConfig { /// IPs within the specified CIDR ranges will be allowed. #[cfg_attr(feature = "serde", serde(skip))] pub ip_filter: IpFilter, + /// If true, discovered peers without a confirmed ENR [`ForkId`](alloy_eip2124::ForkId) + /// (EIP-868) will not be added to the peer set until their fork ID is verified. + /// + /// This filters out peers from other networks that pollute the discovery table. + pub enforce_enr_fork_id: bool, } impl Default for PeersConfig { @@ -191,6 +196,7 @@ impl Default for PeersConfig { max_backoff_count: 5, incoming_ip_throttle_duration: INBOUND_IP_THROTTLE_DURATION, ip_filter: IpFilter::default(), + enforce_enr_fork_id: false, } } } @@ -314,6 +320,13 @@ impl PeersConfig { self } + /// If set, discovered peers without a confirmed ENR [`ForkId`](alloy_eip2124::ForkId) will not + /// be added to the peer set until their fork ID is verified via EIP-868. + pub const fn with_enforce_enr_fork_id(mut self, enforce: bool) -> Self { + self.enforce_enr_fork_id = enforce; + self + } + /// Returns settings for testing #[cfg(any(test, feature = "test-utils"))] pub fn test() -> Self { diff --git a/crates/net/network/src/discovery.rs b/crates/net/network/src/discovery.rs index d032a0a588..c1ea8f1195 100644 --- a/crates/net/network/src/discovery.rs +++ b/crates/net/network/src/discovery.rs @@ -240,7 +240,7 @@ impl Discovery { self.on_node_record_update(record, None); } DiscoveryUpdate::EnrForkId(node, fork_id) => { - self.queued_events.push_back(DiscoveryEvent::EnrForkId(node.id, fork_id)) + self.queued_events.push_back(DiscoveryEvent::EnrForkId(node, fork_id)) } DiscoveryUpdate::Removed(peer_id) => { self.discovered_nodes.remove(&peer_id); diff --git a/crates/net/network/src/peers.rs b/crates/net/network/src/peers.rs index 049f15e907..ba26a99228 100644 --- a/crates/net/network/src/peers.rs +++ b/crates/net/network/src/peers.rs @@ -92,6 +92,9 @@ pub struct PeersManager { incoming_ip_throttle_duration: Duration, /// IP address filter for restricting network connections to specific IP ranges. ip_filter: reth_net_banlist::IpFilter, + /// If true, discovered peers without a confirmed ENR fork ID will not be added until their + /// fork ID is verified via EIP-868. + enforce_enr_fork_id: bool, } impl PeersManager { @@ -111,6 +114,7 @@ impl PeersManager { max_backoff_count, incoming_ip_throttle_duration, ip_filter, + enforce_enr_fork_id, } = config; let (manager_tx, handle_rx) = mpsc::unbounded_channel(); let now = Instant::now(); @@ -167,6 +171,7 @@ impl PeersManager { net_connection_state: NetworkConnectionState::default(), incoming_ip_throttle_duration, ip_filter, + enforce_enr_fork_id, } } @@ -175,6 +180,11 @@ impl PeersManager { PeersHandle::new(self.manager_tx.clone()) } + /// Returns `true` if discovered peers must have a confirmed ENR fork ID before being added. + pub(crate) const fn enforce_enr_fork_id(&self) -> bool { + self.enforce_enr_fork_id + } + /// Returns the number of peers in the peer set #[inline] pub(crate) fn num_known_peers(&self) -> usize { @@ -738,17 +748,6 @@ impl PeersManager { } } - /// Called as follow-up for a discovered peer. - /// - /// The [`ForkId`] is retrieved from an ENR record that the peer announces over the discovery - /// protocol - pub(crate) fn set_discovered_fork_id(&mut self, peer_id: PeerId, fork_id: ForkId) { - if let Some(peer) = self.peers.get_mut(&peer_id) { - trace!(target: "net::peers", ?peer_id, ?fork_id, "set discovered fork id"); - peer.fork_id = Some(Box::new(fork_id)); - } - } - /// Called for a newly discovered peer. /// /// If the peer already exists, then the address, kind and `fork_id` will be updated. diff --git a/crates/net/network/src/state.rs b/crates/net/network/src/state.rs index 84a3e86489..be29d24db3 100644 --- a/crates/net/network/src/state.rs +++ b/crates/net/network/src/state.rs @@ -332,9 +332,19 @@ impl NetworkState { fork_id, }); } - DiscoveryEvent::EnrForkId(peer_id, fork_id) => { - self.queued_messages - .push_back(StateAction::DiscoveredEnrForkId { peer_id, fork_id }); + DiscoveryEvent::EnrForkId(record, fork_id) => { + let peer_id = record.id; + let tcp_addr = record.tcp_addr(); + if tcp_addr.port() == 0 { + return + } + let udp_addr = record.udp_addr(); + let addr = PeerAddr::new(tcp_addr, Some(udp_addr)); + self.queued_messages.push_back(StateAction::DiscoveredEnrForkId { + peer_id, + addr, + fork_id, + }); } } } @@ -552,6 +562,8 @@ pub(crate) enum StateAction { /// Retrieved a [`ForkId`] from the peer via ENR request, See DiscoveredEnrForkId { peer_id: PeerId, + /// The address of the peer. + addr: PeerAddr, /// The reported [`ForkId`] by this peer. fork_id: ForkId, }, diff --git a/crates/net/network/src/swarm.rs b/crates/net/network/src/swarm.rs index a1c4117400..6b13d24fb8 100644 --- a/crates/net/network/src/swarm.rs +++ b/crates/net/network/src/swarm.rs @@ -246,18 +246,28 @@ impl Swarm { StateAction::PeerAdded(peer_id) => return Some(SwarmEvent::PeerAdded(peer_id)), StateAction::PeerRemoved(peer_id) => return Some(SwarmEvent::PeerRemoved(peer_id)), StateAction::DiscoveredNode { peer_id, addr, fork_id } => { - // Don't try to connect to peer if node is shutting down if self.is_shutting_down() { return None } - // Insert peer only if no fork id or a valid fork id - if fork_id.map_or_else(|| true, |f| self.sessions.is_valid_fork_id(f)) { + + // When `enforce_enr_fork_id` is enabled, peers discovered without a confirmed + // fork ID (via EIP-868 ENR) are deferred — they'll only be added once a + // `DiscoveredEnrForkId` event arrives with a validated fork ID. + // + // When disabled (default), peers without a fork ID are admitted immediately. + // Peers that *do* carry a fork ID are always validated against ours. + let enforce = self.state().peers().enforce_enr_fork_id(); + let allow = match fork_id { + Some(f) => self.sessions.is_valid_fork_id(f), + None => !enforce, + }; + if allow { self.state_mut().peers_mut().add_peer(peer_id, addr, fork_id); } } - StateAction::DiscoveredEnrForkId { peer_id, fork_id } => { + StateAction::DiscoveredEnrForkId { peer_id, addr, fork_id } => { if self.sessions.is_valid_fork_id(fork_id) { - self.state_mut().peers_mut().set_discovered_fork_id(peer_id, fork_id); + self.state_mut().peers_mut().add_peer(peer_id, addr, Some(fork_id)); } else { trace!(target: "net", ?peer_id, remote_fork_id=?fork_id, our_fork_id=?self.sessions.fork_id(), "fork id mismatch, removing peer"); self.state_mut().peers_mut().remove_peer(peer_id); diff --git a/crates/node/core/src/args/network.rs b/crates/node/core/src/args/network.rs index 619a79bb81..a99cb16b7c 100644 --- a/crates/node/core/src/args/network.rs +++ b/crates/node/core/src/args/network.rs @@ -227,6 +227,14 @@ pub struct NetworkArgs { /// Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" #[arg(long, value_name = "NETRESTRICT")] pub netrestrict: Option, + + /// Enforce EIP-868 ENR fork ID validation for discovered peers. + /// + /// When enabled, peers discovered without a confirmed fork ID are not added to the peer set + /// until their fork ID is verified via EIP-868 ENR request. This filters out peers from other + /// networks that pollute the discovery table. + #[arg(long)] + pub enforce_enr_fork_id: bool, } impl NetworkArgs { @@ -333,7 +341,8 @@ impl NetworkArgs { ) .with_max_inbound_opt(self.resolved_max_inbound_peers()) .with_max_outbound_opt(self.resolved_max_outbound_peers()) - .with_ip_filter(ip_filter); + .with_ip_filter(ip_filter) + .with_enforce_enr_fork_id(self.enforce_enr_fork_id); // Configure basic network stack NetworkConfigBuilder::::new(secret_key) @@ -491,6 +500,7 @@ impl Default for NetworkArgs { required_block_hashes: vec![], network_id: None, netrestrict: None, + enforce_enr_fork_id: false, } } } diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index 702a1023e6..b9d55a1693 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -293,6 +293,11 @@ Networking: Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + --enforce-enr-fork-id + Enforce EIP-868 ENR fork ID validation for discovered peers. + + When enabled, peers discovered without a confirmed fork ID are not added to the peer set until their fork ID is verified via EIP-868 ENR request. This filters out peers from other networks that pollute the discovery table. + RPC: --http Enable the HTTP-RPC server diff --git a/docs/vocs/docs/pages/cli/reth/p2p/body.mdx b/docs/vocs/docs/pages/cli/reth/p2p/body.mdx index e0308bb255..c958c8cc25 100644 --- a/docs/vocs/docs/pages/cli/reth/p2p/body.mdx +++ b/docs/vocs/docs/pages/cli/reth/p2p/body.mdx @@ -233,6 +233,11 @@ Networking: Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + --enforce-enr-fork-id + Enforce EIP-868 ENR fork ID validation for discovered peers. + + When enabled, peers discovered without a confirmed fork ID are not added to the peer set until their fork ID is verified via EIP-868 ENR request. This filters out peers from other networks that pollute the discovery table. + Datadir: --datadir The path to the data dir for all reth files and subdirectories. diff --git a/docs/vocs/docs/pages/cli/reth/p2p/header.mdx b/docs/vocs/docs/pages/cli/reth/p2p/header.mdx index 101724c438..2f80a07cb4 100644 --- a/docs/vocs/docs/pages/cli/reth/p2p/header.mdx +++ b/docs/vocs/docs/pages/cli/reth/p2p/header.mdx @@ -233,6 +233,11 @@ Networking: Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + --enforce-enr-fork-id + Enforce EIP-868 ENR fork ID validation for discovered peers. + + When enabled, peers discovered without a confirmed fork ID are not added to the peer set until their fork ID is verified via EIP-868 ENR request. This filters out peers from other networks that pollute the discovery table. + Datadir: --datadir The path to the data dir for all reth files and subdirectories. diff --git a/docs/vocs/docs/pages/cli/reth/stage/run.mdx b/docs/vocs/docs/pages/cli/reth/stage/run.mdx index 34d8f67ae3..cade11e4c3 100644 --- a/docs/vocs/docs/pages/cli/reth/stage/run.mdx +++ b/docs/vocs/docs/pages/cli/reth/stage/run.mdx @@ -460,6 +460,11 @@ Networking: Example: --netrestrict "192.168.0.0/16,10.0.0.0/8" + --enforce-enr-fork-id + Enforce EIP-868 ENR fork ID validation for discovered peers. + + When enabled, peers discovered without a confirmed fork ID are not added to the peer set until their fork ID is verified via EIP-868 ENR request. This filters out peers from other networks that pollute the discovery table. + Logging: --log.stdout.format The format to use for logs written to stdout