Skip to main content

reth_discv5/
lib.rs

1//! Wrapper around [`discv5::Discv5`].
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10
11use std::{
12    collections::HashSet,
13    fmt,
14    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
15    sync::Arc,
16    time::Duration,
17};
18
19use ::enr::Enr;
20use alloy_primitives::bytes::Bytes;
21use enr::{discv4_id_to_discv5_id, EnrCombinedKeyWrapper};
22use futures::future::join_all;
23use itertools::Itertools;
24use rand::{Rng, RngCore};
25use reth_ethereum_forks::{EnrForkIdEntry, ForkId};
26use reth_network_peers::{NodeRecord, PeerId};
27use secp256k1::SecretKey;
28use tokio::{sync::mpsc, task};
29use tracing::{debug, error, trace};
30
31pub mod config;
32pub mod enr;
33pub mod error;
34pub mod filter;
35pub mod metrics;
36pub mod network_stack_id;
37
38pub use discv5::{self, IpMode};
39
40pub use config::{
41    BootNode, Config, ConfigBuilder, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_ADDR,
42    DEFAULT_DISCOVERY_V5_ADDR_IPV6, DEFAULT_DISCOVERY_V5_LISTEN_CONFIG, DEFAULT_DISCOVERY_V5_PORT,
43    DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL,
44};
45pub use enr::enr_to_discv4_id;
46pub use error::Error;
47pub use filter::{FilterOutcome, MustNotIncludeKeys};
48pub use network_stack_id::NetworkStackId;
49
50use metrics::{DiscoveredPeersMetrics, Discv5Metrics};
51
52/// Max kbucket index is 255.
53///
54/// This is the max log2distance for 32 byte [`NodeId`](discv5::enr::NodeId) - 1. See <https://github.com/sigp/discv5/blob/e9e0d4f93ec35591832a9a8d937b4161127da87b/src/kbucket.rs#L586-L587>.
55pub const MAX_KBUCKET_INDEX: usize = 255;
56
57/// Default lowest kbucket index to attempt filling, in periodic look up query to populate kbuckets.
58///
59/// The peer at the 0th kbucket index is at log2distance 1 from the local node ID. See <https://github.com/sigp/discv5/blob/e9e0d4f93ec35591832a9a8d937b4161127da87b/src/kbucket.rs#L586-L587>.
60///
61/// Default is 0th index.
62pub const DEFAULT_MIN_TARGET_KBUCKET_INDEX: usize = 0;
63
64/// Transparent wrapper around [`discv5::Discv5`].
65#[derive(Clone)]
66pub struct Discv5 {
67    /// sigp/discv5 node.
68    discv5: Arc<discv5::Discv5>,
69    /// [`IpMode`] of the `RLPx` network.
70    rlpx_ip_mode: IpMode,
71    /// Key used in kv-pair to ID chain, e.g. 'opstack' or 'eth'.
72    fork_key: Option<&'static [u8]>,
73    /// Filter applied to a discovered peers before passing it up to app.
74    discovered_peer_filter: MustNotIncludeKeys,
75    /// Metrics for underlying [`discv5::Discv5`] node and filtered discovered peers.
76    metrics: Discv5Metrics,
77    /// Returns the _local_ [`NodeRecord`] this service was started with.
78    // Note: we must track this separately because the `discv5::Discv5` does not necessarily
79    // provide this via its [`local_enr`](discv5::Discv5::local_enr()). This is intended for
80    // obtaining the port this service was launched at
81    local_node_record: NodeRecord,
82}
83
84impl Discv5 {
85    ////////////////////////////////////////////////////////////////////////////////////////////////
86    // Minimal interface with `reth_network::discovery`
87    ////////////////////////////////////////////////////////////////////////////////////////////////
88
89    /// Adds the node to the table, if it is not already present.
90    pub fn add_node(&self, node_record: Enr<SecretKey>) -> Result<(), Error> {
91        let EnrCombinedKeyWrapper(enr) = node_record.into();
92        self.discv5.add_enr(enr).map_err(Error::AddNodeFailed)
93    }
94
95    /// Sets the pair in the EIP-868 [`Enr`] of the node.
96    ///
97    /// If the key already exists, this will update it.
98    ///
99    /// CAUTION: The value **must** be rlp encoded
100    pub fn set_eip868_in_local_enr(&self, key: Vec<u8>, rlp: Bytes) {
101        let Ok(key_str) = std::str::from_utf8(&key) else {
102            error!(target: "net::discv5",
103                err="key not utf-8",
104                "failed to update local enr"
105            );
106            return
107        };
108        if let Err(err) = self.discv5.enr_insert(key_str, &rlp) {
109            error!(target: "net::discv5",
110                %err,
111                "failed to update local enr"
112            );
113        }
114    }
115
116    /// Sets the pair in the EIP-868 [`Enr`] of the node.
117    ///
118    /// If the key already exists, this will update it.
119    pub fn encode_and_set_eip868_in_local_enr(
120        &self,
121        key: Vec<u8>,
122        value: impl alloy_rlp::Encodable,
123    ) {
124        let mut buf = Vec::new();
125        value.encode(&mut buf);
126        self.set_eip868_in_local_enr(key, buf.into())
127    }
128
129    /// Adds the peer and id to the ban list.
130    ///
131    /// This will prevent any future inclusion in the table
132    pub fn ban(&self, peer_id: PeerId, ip: IpAddr) {
133        match discv4_id_to_discv5_id(peer_id) {
134            Ok(node_id) => {
135                self.discv5.ban_node(&node_id, None);
136                self.ban_ip(ip);
137            }
138            Err(err) => error!(target: "net::discv5",
139                %err,
140                "failed to ban peer"
141            ),
142        }
143    }
144
145    /// Adds the ip to the ban list.
146    ///
147    /// This will prevent any future inclusion in the table
148    pub fn ban_ip(&self, ip: IpAddr) {
149        self.discv5.ban_ip(ip, None);
150    }
151
152    /// Returns the [`NodeRecord`] of the local node.
153    ///
154    /// This includes the currently tracked external IP address of the node.
155    ///
156    /// Returns `None` if the local ENR does not contain the required fields.
157    pub fn node_record(&self) -> Option<NodeRecord> {
158        let enr: Enr<_> = EnrCombinedKeyWrapper(self.discv5.local_enr()).into();
159        enr.try_into().ok()
160    }
161
162    /// Returns the local [`Enr`] of the service.
163    pub fn local_enr(&self) -> Enr<discv5::enr::CombinedKey> {
164        self.discv5.local_enr()
165    }
166
167    /// The port the discv5 service is listening on.
168    pub const fn local_port(&self) -> u16 {
169        self.local_node_record.udp_port
170    }
171
172    /// Spawns [`discv5::Discv5`]. Returns [`discv5::Discv5`] handle in reth compatible wrapper type
173    /// [`Discv5`], a receiver of [`discv5::Event`]s from the underlying node, and the local
174    /// [`Enr`](discv5::Enr) converted into the reth compatible [`NodeRecord`] type.
175    pub async fn start(
176        sk: &SecretKey,
177        discv5_config: Config,
178    ) -> Result<(Self, mpsc::Receiver<discv5::Event>), Error> {
179        //
180        // 1. make local enr from listen config
181        //
182        let (enr, local_node_record, fork_key, rlpx_ip_mode) = build_local_enr(sk, &discv5_config);
183
184        trace!(target: "net::discv5", ?enr, "local ENR");
185
186        //
187        // 2. start discv5
188        //
189        let Config {
190            discv5_config,
191            bootstrap_nodes,
192            lookup_interval,
193            bootstrap_lookup_interval,
194            bootstrap_lookup_countdown,
195            discovered_peer_filter,
196            ..
197        } = discv5_config;
198
199        let EnrCombinedKeyWrapper(enr) = enr.into();
200        let sk = discv5::enr::CombinedKey::secp256k1_from_bytes(&mut sk.secret_bytes()).unwrap();
201        let mut discv5 = match discv5::Discv5::new(enr, sk, discv5_config) {
202            Ok(discv5) => discv5,
203            Err(err) => return Err(Error::InitFailure(err)),
204        };
205        discv5.start().await.map_err(Error::Discv5Error)?;
206
207        // start discv5 updates stream
208        let discv5_updates = discv5.event_stream().await.map_err(Error::Discv5Error)?;
209
210        let discv5 = Arc::new(discv5);
211
212        //
213        // 3. add boot nodes
214        //
215        bootstrap(bootstrap_nodes, &discv5).await?;
216
217        let metrics = Discv5Metrics::default();
218
219        //
220        // 4. start bg kbuckets maintenance
221        //
222        spawn_populate_kbuckets_bg(
223            lookup_interval,
224            bootstrap_lookup_interval,
225            bootstrap_lookup_countdown,
226            metrics.clone(),
227            Arc::downgrade(&discv5),
228        );
229
230        Ok((
231            Self {
232                discv5,
233                rlpx_ip_mode,
234                fork_key,
235                discovered_peer_filter,
236                metrics,
237                local_node_record,
238            },
239            discv5_updates,
240        ))
241    }
242
243    /// Process an event from the underlying [`discv5::Discv5`] node.
244    pub fn on_discv5_update(&self, update: discv5::Event) -> Option<DiscoveredPeer> {
245        #[expect(clippy::match_same_arms)]
246        match update {
247            discv5::Event::SocketUpdated(_) | discv5::Event::TalkRequest(_) |
248            // `Discovered` not unique discovered peers
249            discv5::Event::Discovered(_) |
250            // Unrecognized frames are handled separately by the discovery layer
251            discv5::Event::UnrecognizedFrame(_) => None,
252            discv5::Event::NodeInserted { .. } => {
253
254                // node has been inserted into kbuckets
255
256                // `replaced` partly covers `reth_discv4::DiscoveryUpdate::Removed(_)`
257
258                self.metrics.discovered_peers.increment_kbucket_insertions(1);
259
260                None
261            }
262            discv5::Event::SessionEstablished(enr, remote_socket) => {
263                // this branch is semantically similar to branches of
264                // `reth_discv4::DiscoveryUpdate`: `DiscoveryUpdate::Added(_)` and
265                // `DiscoveryUpdate::DiscoveredAtCapacity(_)
266
267                // peer has been discovered as part of query, or, by incoming session (peer has
268                // discovered us)
269
270                self.metrics.discovered_peers.increment_established_sessions_raw(1);
271
272                self.on_discovered_peer(&enr, remote_socket)
273            }
274            discv5::Event::UnverifiableEnr {
275                enr,
276                socket,
277                node_id: _,
278            } => {
279                // this branch is semantically similar to branches of
280                // `reth_discv4::DiscoveryUpdate`: `DiscoveryUpdate::Added(_)` and
281                // `DiscoveryUpdate::DiscoveredAtCapacity(_)
282
283                // peer has been discovered as part of query, or, by an outgoing session (but peer
284                // is behind NAT and responds from a different socket)
285
286                // NOTE: `discv5::Discv5` won't initiate a session with any peer with an
287                // unverifiable node record, for example one that advertises a reserved LAN IP
288                // address on a WAN network. This is in order to prevent DoS attacks, where some
289                // malicious peers may advertise a victim's socket. We will still try and connect
290                // to them over RLPx, to be compatible with EL discv5 implementations that don't
291                // enforce this security measure.
292
293                trace!(target: "net::discv5",
294                    ?enr,
295                    %socket,
296                    "discovered unverifiable enr, source socket doesn't match socket advertised in ENR"
297                );
298
299                self.metrics.discovered_peers.increment_unverifiable_enrs_raw_total(1);
300
301                self.on_discovered_peer(&enr, socket)
302            }
303            _ => None
304        }
305    }
306
307    /// Processes a discovered peer. Returns `true` if peer is added to
308    pub fn on_discovered_peer(
309        &self,
310        enr: &discv5::Enr,
311        socket: SocketAddr,
312    ) -> Option<DiscoveredPeer> {
313        self.metrics.discovered_peers_advertised_networks.increment_once_by_network_type(enr);
314
315        let node_record = match self.try_into_reachable(enr, socket) {
316            Ok(enr_bc) => enr_bc,
317            Err(err) => {
318                trace!(target: "net::discv5",
319                    %err,
320                    ?enr,
321                    "discovered peer is unreachable"
322                );
323
324                self.metrics.discovered_peers.increment_established_sessions_unreachable_enr(1);
325
326                return None
327            }
328        };
329        if let FilterOutcome::Ignore { reason } = self.filter_discovered_peer(enr) {
330            trace!(target: "net::discv5",
331                ?enr,
332                reason,
333                "filtered out discovered peer"
334            );
335
336            self.metrics.discovered_peers.increment_established_sessions_filtered(1);
337
338            return None
339        }
340
341        let fork_id = self.get_fork_id(enr).ok();
342
343        trace!(target: "net::discv5",
344            ?fork_id,
345            ?enr,
346            "discovered peer"
347        );
348
349        Some(DiscoveredPeer { node_record, fork_id })
350    }
351
352    /// Tries to recover an unreachable [`Enr`](discv5::Enr) received via
353    /// [`discv5::Event::UnverifiableEnr`], into a [`NodeRecord`] usable by `RLPx`.
354    ///
355    /// NOTE: Fallback solution to be compatible with Geth which includes peers into the discv5
356    /// WAN topology which, for example, advertise in their ENR that localhost is their UDP IP
357    /// address. These peers are only discovered if they initiate a connection attempt, and we by
358    /// such means learn their reachable IP address. If we receive their ENR from any other peer
359    /// as part of a lookup query, we won't find a reachable IP address on which to dial them by
360    /// reading their ENR.
361    pub fn try_into_reachable(
362        &self,
363        enr: &discv5::Enr,
364        socket: SocketAddr,
365    ) -> Result<NodeRecord, Error> {
366        // Use the sender's address as received so discovery IP bans match source sockets.
367        let address = socket.ip();
368        let udp_port = socket.port();
369
370        let id = enr_to_discv4_id(enr).ok_or(Error::IncompatibleKeyType)?;
371
372        // The port must pair with the session socket's address, so select the
373        // ENR field by the socket's address family, not by the local node's
374        // RLPx listen family: an IPv4 session dials the `tcp` port, an IPv6
375        // session the `tcp6` port (falling back to `tcp`).
376        // Mapped IPv4 sessions use `tcp`, while the record retains the original socket address.
377        let tcp_port = (match address.to_canonical() {
378            IpAddr::V4(_) => enr.tcp4(),
379            IpAddr::V6(_) => enr.tcp6().or_else(|| enr.tcp4()),
380        })
381        .unwrap_or(
382            // tcp socket is missing from ENR for the session's address family.
383            //
384            // by default geth runs discv5 and discv4 behind the same udp port (the discv4 default
385            // port 30303), so rlpx has a chance of successfully dialing the peer on its discv5
386            // udp port if its running geth's p2p code.
387            udp_port,
388        );
389
390        Ok(NodeRecord { address, tcp_port, udp_port, id })
391    }
392
393    /// Applies filtering rules on an ENR. Returns [`Ok`](FilterOutcome::Ok) if peer should be
394    /// passed up to app, and [`Ignore`](FilterOutcome::Ignore) if peer should instead be dropped.
395    pub fn filter_discovered_peer(&self, enr: &discv5::Enr) -> FilterOutcome {
396        self.discovered_peer_filter.filter(enr)
397    }
398
399    /// Returns the [`ForkId`] of the given [`Enr`](discv5::Enr) w.r.t. the local node's network
400    /// stack, if field is set.
401    pub fn get_fork_id<K: discv5::enr::EnrKey>(
402        &self,
403        enr: &discv5::enr::Enr<K>,
404    ) -> Result<ForkId, Error> {
405        let Some(key) = self.fork_key else { return Err(Error::NetworkStackIdNotConfigured) };
406        let fork_id = enr
407            .get_decodable::<EnrForkIdEntry>(key)
408            .or_else(|| {
409                (key != NetworkStackId::ETH)
410                    .then(|| {
411                        // Fallback: trying to get fork id from Enr with 'eth' as network stack id
412                        trace!(target: "net::discv5",
413                            key = %String::from_utf8_lossy(key),
414                            "Fork id not found for key, trying 'eth'..."
415                        );
416                        enr.get_decodable::<EnrForkIdEntry>(NetworkStackId::ETH)
417                    })
418                    .flatten()
419            })
420            .ok_or({
421                trace!(target: "net::discv5", "Fork id not found for 'eth' network stack id");
422                Error::ForkMissing(key)
423            })?
424            .map(Into::into)?;
425
426        Ok(fork_id)
427    }
428
429    ////////////////////////////////////////////////////////////////////////////////////////////////
430    // Interface with sigp/discv5
431    ////////////////////////////////////////////////////////////////////////////////////////////////
432
433    /// Exposes API of [`discv5::Discv5`].
434    pub fn with_discv5<F, R>(&self, f: F) -> R
435    where
436        F: FnOnce(&discv5::Discv5) -> R,
437    {
438        f(&self.discv5)
439    }
440
441    ////////////////////////////////////////////////////////////////////////////////////////////////
442    // Complementary
443    ////////////////////////////////////////////////////////////////////////////////////////////////
444
445    /// Returns the `RLPx` [`IpMode`] of the local node.
446    pub const fn ip_mode(&self) -> IpMode {
447        self.rlpx_ip_mode
448    }
449
450    /// Returns the key to use to identify the [`ForkId`] kv-pair on the [`Enr`](discv5::Enr).
451    pub const fn fork_key(&self) -> Option<&[u8]> {
452        self.fork_key
453    }
454}
455
456impl fmt::Debug for Discv5 {
457    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458        "{ .. }".fmt(f)
459    }
460}
461
462/// Result of successfully processing a peer discovered by [`discv5::Discv5`].
463#[derive(Debug)]
464pub struct DiscoveredPeer {
465    /// A discovery v4 backwards compatible ENR.
466    pub node_record: NodeRecord,
467    /// [`ForkId`] extracted from ENR w.r.t. configured
468    pub fork_id: Option<ForkId>,
469}
470
471/// Builds the local ENR with the supplied key.
472pub fn build_local_enr(
473    sk: &SecretKey,
474    config: &Config,
475) -> (Enr<SecretKey>, NodeRecord, Option<&'static [u8]>, IpMode) {
476    let mut builder = discv5::enr::Enr::builder();
477
478    let Config {
479        discv5_config,
480        fork,
481        tcp_socket,
482        advertised_ipv4,
483        advertised_ipv6,
484        other_enr_kv_pairs,
485        ..
486    } = config;
487
488    let socket = {
489        let v4 = crate::config::ipv4(&discv5_config.listen_config);
490        let v6 = crate::config::ipv6(&discv5_config.listen_config);
491
492        // Prefer an explicit advertised IP for ENR IP fields. Listen sockets still supply UDP
493        // ports and determine which address-family fields are emitted.
494        if let Some(addr) = v4 {
495            if let Some(ip) = advertised_ipv4 {
496                builder.ip4(*ip);
497            } else if *addr.ip() != Ipv4Addr::UNSPECIFIED {
498                builder.ip4(*addr.ip());
499            }
500            builder.udp4(addr.port());
501        }
502        if let Some(addr) = v6 {
503            if let Some(ip) = advertised_ipv6 {
504                builder.ip6(*ip);
505            } else if *addr.ip() != Ipv6Addr::UNSPECIFIED {
506                builder.ip6(*addr.ip());
507            }
508            builder.udp6(addr.port());
509        }
510        // Advertise tcp4 when v4 is configured, else tcp6.
511        if v4.is_some() {
512            builder.tcp4(tcp_socket.port());
513        } else if v6.is_some() {
514            builder.tcp6(tcp_socket.port());
515        }
516
517        // Prefer v6 when both are configured
518        v6.map(SocketAddr::V6)
519            .or_else(|| v4.map(SocketAddr::V4))
520            .unwrap_or_else(|| SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)))
521    };
522
523    let rlpx_ip_mode = if tcp_socket.is_ipv4() { IpMode::Ip4 } else { IpMode::Ip6 };
524
525    // identifies which network node is on
526    let network_stack_id = fork.as_ref().map(|(network_stack_id, fork_value)| {
527        builder.add_value_rlp(network_stack_id, alloy_rlp::encode(fork_value).into());
528        *network_stack_id
529    });
530
531    // add other data
532    for (key, value) in other_enr_kv_pairs {
533        builder.add_value_rlp(key, value.clone().into());
534    }
535
536    // enr v4 not to get confused with discv4, independent versioning enr and
537    // discovery
538    let enr = builder.build(sk).expect("should build enr v4");
539
540    // backwards compatible enr
541    let bc_enr = NodeRecord::from_secret_key(socket, sk);
542
543    (enr, bc_enr, network_stack_id, rlpx_ip_mode)
544}
545
546/// Bootstraps underlying [`discv5::Discv5`] node with configured peers.
547///
548/// Boot nodes given as an ENR are added to the kbuckets right away. Boot nodes given as an enode
549/// need their ENR requested over the wire first, which is driven in the background: an unreachable
550/// boot node only answers after the discv5 request timeout, and waiting for that would stall node
551/// startup.
552///
553/// Stays `async` because spawning those requests requires a tokio runtime context.
554pub async fn bootstrap(
555    bootstrap_nodes: HashSet<BootNode>,
556    discv5: &Arc<discv5::Discv5>,
557) -> Result<(), Error> {
558    trace!(target: "net::discv5",
559        ?bootstrap_nodes,
560        "adding bootstrap nodes .."
561    );
562
563    let mut enr_requests = vec![];
564    for node in bootstrap_nodes {
565        match node {
566            BootNode::Enr(node) => {
567                if let Err(err) = discv5.add_enr(node) {
568                    return Err(Error::AddNodeFailed(err))
569                }
570            }
571            BootNode::Enode(enode) => {
572                let discv5 = discv5.clone();
573                enr_requests.push(async move {
574                    if let Err(err) = discv5.request_enr(enode.to_string()).await {
575                        debug!(target: "net::discv5",
576                            ?enode,
577                            %err,
578                            "failed adding boot node"
579                        );
580                    }
581                })
582            }
583        }
584    }
585
586    // If a session is established, the ENR is added straight away to discv5 kbuckets
587    if !enr_requests.is_empty() {
588        task::spawn(join_all(enr_requests));
589    }
590
591    Ok(())
592}
593
594/// Backgrounds regular look up queries, in order to keep kbuckets populated.
595pub fn spawn_populate_kbuckets_bg(
596    lookup_interval: u64,
597    bootstrap_lookup_interval: u64,
598    bootstrap_lookup_countdown: u64,
599    metrics: Discv5Metrics,
600    discv5: std::sync::Weak<discv5::Discv5>,
601) {
602    let lookup_interval = Duration::from_secs(lookup_interval);
603    let metrics = metrics.discovered_peers;
604    let mut kbucket_index = MAX_KBUCKET_INDEX;
605    let pulse_lookup_interval = Duration::from_secs(bootstrap_lookup_interval);
606    task::spawn(async move {
607        let Some(discv5_handle) = discv5.upgrade() else {
608            return;
609        };
610        let local_node_id = discv5_handle.local_enr().node_id();
611        drop(discv5_handle);
612
613        // make many fast lookup queries at bootstrap, trying to fill kbuckets at furthest
614        // log2distance from local node
615        for i in (0..bootstrap_lookup_countdown).rev() {
616            let target = discv5::enr::NodeId::random();
617
618            trace!(target: "net::discv5",
619                %target,
620                bootstrap_boost_runs_countdown=i,
621                lookup_interval=format!("{:#?}", pulse_lookup_interval),
622                "starting bootstrap boost lookup query"
623            );
624
625            {
626                let Some(discv5_handle) = discv5.upgrade() else {
627                    return;
628                };
629                lookup(target, &discv5_handle, &metrics).await;
630            }
631
632            tokio::time::sleep(pulse_lookup_interval).await;
633        }
634
635        // initiate regular lookups to populate kbuckets
636        loop {
637            // make sure node is connected to each subtree in the network by target
638            // selection (ref kademlia)
639            let target = get_lookup_target(kbucket_index, local_node_id);
640
641            trace!(target: "net::discv5",
642                %target,
643                lookup_interval=format!("{:#?}", lookup_interval),
644                "starting periodic lookup query"
645            );
646
647            {
648                let Some(discv5_handle) = discv5.upgrade() else {
649                    return;
650                };
651                lookup(target, &discv5_handle, &metrics).await;
652            }
653
654            if kbucket_index > DEFAULT_MIN_TARGET_KBUCKET_INDEX {
655                // try to populate bucket one step closer
656                kbucket_index -= 1
657            } else {
658                // start over with bucket furthest away
659                kbucket_index = MAX_KBUCKET_INDEX
660            }
661
662            tokio::time::sleep(lookup_interval).await;
663        }
664    });
665}
666
667/// Gets the next lookup target, based on which bucket is currently being targeted.
668pub fn get_lookup_target(
669    kbucket_index: usize,
670    local_node_id: discv5::enr::NodeId,
671) -> discv5::enr::NodeId {
672    // init target
673    let mut target = local_node_id.raw();
674
675    // make sure target has a 'log2distance'-long suffix that differs from local node id
676    let bit_offset = MAX_KBUCKET_INDEX.saturating_sub(kbucket_index);
677    let (byte, bit) = (bit_offset / 8, bit_offset % 8);
678    // Flip the target bit.
679    target[byte] ^= 1 << (7 - bit);
680
681    // Randomize the bits after the target.
682    let mut rng = rand::rng();
683    // Randomize remaining bits in the byte we modified.
684    if bit < 7 {
685        // Compute the mask of the bits that need to be randomized.
686        let bits_to_randomize = 0xff >> (bit + 1);
687        // Clear.
688        target[byte] &= !bits_to_randomize;
689        // Randomize.
690        target[byte] |= rng.random::<u8>() & bits_to_randomize;
691    }
692    // Randomize remaining bytes.
693    rng.fill_bytes(&mut target[byte + 1..]);
694
695    target.into()
696}
697
698/// Runs a [`discv5::Discv5`] lookup query.
699pub async fn lookup(
700    target: discv5::enr::NodeId,
701    discv5: &discv5::Discv5,
702    metrics: &DiscoveredPeersMetrics,
703) {
704    metrics.set_total_sessions(discv5.metrics().active_sessions);
705    metrics.set_total_kbucket_peers(
706        discv5.with_kbuckets(|kbuckets| kbuckets.read().iter_ref().count()),
707    );
708
709    match discv5.find_node(target).await {
710        Err(err) => trace!(target: "net::discv5",
711            %err,
712            "lookup query failed"
713        ),
714        Ok(peers) => trace!(target: "net::discv5",
715            target=format!("{:#?}", target),
716            peers_count=peers.len(),
717            peers=format!("[{:#}]", peers.iter()
718                .map(|enr| enr.node_id()
719            ).format(", ")),
720            "peers returned by lookup query"
721        ),
722    }
723
724    // `Discv5::connected_peers` can be subset of sessions, not all peers make it
725    // into kbuckets, e.g. incoming sessions from peers with
726    // unreachable enrs
727    debug!(target: "net::discv5",
728        connected_peers=discv5.connected_peers(),
729        "connected peers in routing table"
730    );
731}
732
733#[cfg(test)]
734mod test {
735    #![allow(deprecated)]
736    use super::*;
737    use ::enr::{CombinedKey, EnrKey};
738    use discv5::ListenConfig;
739    use rand_08::thread_rng;
740    use reth_chainspec::MAINNET;
741    use std::{
742        net::UdpSocket,
743        time::{Duration, Instant},
744    };
745    use tracing::trace;
746
747    fn discv5_noop() -> Discv5 {
748        let sk = CombinedKey::generate_secp256k1();
749        Discv5 {
750            discv5: Arc::new(
751                discv5::Discv5::new(
752                    Enr::empty(&sk).unwrap(),
753                    sk,
754                    discv5::ConfigBuilder::new(DEFAULT_DISCOVERY_V5_LISTEN_CONFIG).build(),
755                )
756                .unwrap(),
757            ),
758            rlpx_ip_mode: IpMode::Ip4,
759            fork_key: None,
760            discovered_peer_filter: MustNotIncludeKeys::default(),
761            metrics: Discv5Metrics::default(),
762            local_node_record: NodeRecord::new(
763                (Ipv4Addr::LOCALHOST, 30303).into(),
764                PeerId::random(),
765            ),
766        }
767    }
768
769    async fn start_discovery_node(udp_port_discv5: u16) -> (Discv5, mpsc::Receiver<discv5::Event>) {
770        let secret_key = SecretKey::new(&mut thread_rng());
771
772        let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
773        let rlpx_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
774
775        let discv5_listen_config = ListenConfig::from(discv5_addr);
776        let discv5_config = Config::builder(rlpx_addr)
777            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
778            .build();
779
780        Discv5::start(&secret_key, discv5_config).await.expect("should build discv5")
781    }
782
783    async fn start_discovery_node_with_key(
784        secret_key: &SecretKey,
785        udp_port_discv5: u16,
786    ) -> Result<(Discv5, mpsc::Receiver<discv5::Event>), Error> {
787        let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
788        let rlpx_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
789
790        let discv5_listen_config = ListenConfig::from(discv5_addr);
791        let discv5_config = Config::builder(rlpx_addr)
792            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
793            .build();
794
795        Discv5::start(secret_key, discv5_config).await
796    }
797
798    fn unused_udp_port() -> u16 {
799        UdpSocket::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
800    }
801
802    async fn wait_for_udp_port_release(port: u16, timeout: Duration) {
803        let deadline = Instant::now() + timeout;
804
805        loop {
806            match UdpSocket::bind(("127.0.0.1", port)) {
807                Ok(socket) => {
808                    drop(socket);
809                    return;
810                }
811                Err(err) if Instant::now() < deadline => {
812                    trace!(target: "net::discv5::test", %port, %err, "waiting for discv5 port release");
813                    tokio::time::sleep(Duration::from_millis(10)).await;
814                }
815                Err(err) => panic!("discv5 did not release port {port} before timeout: {err}"),
816            }
817        }
818    }
819
820    #[tokio::test(flavor = "multi_thread")]
821    async fn discv5_releases_port_on_drop() {
822        reth_tracing::init_test_tracing();
823
824        let secret_key = SecretKey::new(&mut thread_rng());
825        let port = unused_udp_port();
826
827        let (node, updates) =
828            start_discovery_node_with_key(&secret_key, port).await.expect("should start discv5");
829        drop(updates);
830        drop(node);
831
832        wait_for_udp_port_release(port, Duration::from_secs(1)).await;
833
834        let restarted = start_discovery_node_with_key(&secret_key, port).await;
835        assert!(restarted.is_ok(), "discv5 failed to rebind dropped port: {restarted:?}");
836    }
837
838    #[tokio::test(flavor = "multi_thread")]
839    async fn discv5() {
840        reth_tracing::init_test_tracing();
841
842        // rig test
843
844        // rig node_1
845        let (node_1, mut stream_1) = start_discovery_node(30344).await;
846        let node_1_enr = node_1.with_discv5(|discv5| discv5.local_enr());
847
848        // rig node_2
849        let (node_2, mut stream_2) = start_discovery_node(30355).await;
850        let node_2_enr = node_2.with_discv5(|discv5| discv5.local_enr());
851
852        trace!(target: "net::discv5::test",
853            node_1_node_id=format!("{:#}", node_1_enr.node_id()),
854            node_2_node_id=format!("{:#}", node_2_enr.node_id()),
855            "started nodes"
856        );
857
858        // test
859
860        // add node_2 to discovery handle of node_1 (should add node to discv5 kbuckets)
861        let node_2_enr_reth_compatible_ty: Enr<SecretKey> =
862            EnrCombinedKeyWrapper(node_2_enr.clone()).into();
863        node_1.add_node(node_2_enr_reth_compatible_ty).unwrap();
864
865        // verify node_2 is in KBuckets of node_1:discv5
866        assert!(
867            node_1.with_discv5(|discv5| discv5.table_entries_id().contains(&node_2_enr.node_id()))
868        );
869
870        // manually trigger connection from node_1 to node_2
871        node_1.with_discv5(|discv5| discv5.send_ping(node_2_enr.clone())).await.unwrap();
872
873        // verify node_1:discv5 is connected to node_2:discv5 and vv
874        let event_1_v5 = stream_1.recv().await.unwrap();
875
876        assert!(matches!(
877            event_1_v5,
878            discv5::Event::SessionEstablished(node, socket) if node == node_2_enr && socket == node_2_enr.udp4_socket().unwrap().into()
879        ));
880
881        // verify node_1 is in KBuckets of node_2:discv5
882        let event_2_v5 = stream_2.recv().await.unwrap();
883        assert!(matches!(
884            event_2_v5,
885            discv5::Event::NodeInserted { node_id, replaced } if node_id == node_1_enr.node_id() && replaced.is_none()
886        ));
887    }
888
889    #[test]
890    fn discovered_enr_disc_socket_missing() {
891        reth_tracing::init_test_tracing();
892
893        // rig test
894        const REMOTE_RLPX_PORT: u16 = 30303;
895        let remote_socket = "104.28.44.25:9000".parse().unwrap();
896        let remote_key = CombinedKey::generate_secp256k1();
897        let remote_enr = Enr::builder().tcp4(REMOTE_RLPX_PORT).build(&remote_key).unwrap();
898
899        let discv5 = discv5_noop();
900
901        // test
902        let filtered_peer = discv5.on_discovered_peer(&remote_enr, remote_socket);
903
904        assert_eq!(
905            NodeRecord {
906                address: remote_socket.ip(),
907                udp_port: remote_socket.port(),
908                tcp_port: REMOTE_RLPX_PORT,
909                id: enr_to_discv4_id(&remote_enr).unwrap(),
910            },
911            filtered_peer.unwrap().node_record
912        )
913    }
914
915    #[test]
916    fn reachable_tcp_port_follows_session_address_family() {
917        // Regression test for https://github.com/paradigmxyz/reth/issues/27734:
918        // the TCP port paired with the session socket's IP must come from the
919        // ENR field of the *session's* address family, not the local node's
920        // RLPx listen family.
921        const TCP4_PORT: u16 = 30304;
922        const TCP6_PORT: u16 = 30306;
923        let remote_key = CombinedKey::generate_secp256k1();
924        let remote_enr = Enr::builder().tcp4(TCP4_PORT).tcp6(TCP6_PORT).build(&remote_key).unwrap();
925
926        let v4_socket: SocketAddr = "127.0.0.1:30305".parse().unwrap();
927        let v6_socket: SocketAddr = "[::1]:30307".parse().unwrap();
928
929        for local_mode in [IpMode::Ip4, IpMode::Ip6] {
930            let mut discv5 = discv5_noop();
931            discv5.rlpx_ip_mode = local_mode;
932
933            let record = discv5.try_into_reachable(&remote_enr, v4_socket).unwrap();
934            assert_eq!(
935                record.tcp_port, TCP4_PORT,
936                "v4 session with local mode {local_mode:?} must dial the tcp4 port"
937            );
938            assert_eq!(record.address, v4_socket.ip());
939            assert_eq!(record.udp_port, v4_socket.port());
940
941            let record = discv5.try_into_reachable(&remote_enr, v6_socket).unwrap();
942            assert_eq!(
943                record.tcp_port, TCP6_PORT,
944                "v6 session with local mode {local_mode:?} must dial the tcp6 port"
945            );
946            assert_eq!(record.address, v6_socket.ip());
947            assert_eq!(record.udp_port, v6_socket.port());
948        }
949    }
950
951    #[test]
952    fn reachable_ipv4_mapped_session_uses_tcp4() {
953        const TCP4_PORT: u16 = 30304;
954        const TCP6_PORT: u16 = 30306;
955        let remote_key = CombinedKey::generate_secp256k1();
956        let remote_enr = Enr::builder().tcp4(TCP4_PORT).tcp6(TCP6_PORT).build(&remote_key).unwrap();
957        let mapped_socket: SocketAddr = "[::ffff:104.28.44.25]:9000".parse().unwrap();
958
959        for local_mode in [IpMode::Ip4, IpMode::Ip6] {
960            let mut discv5 = discv5_noop();
961            discv5.rlpx_ip_mode = local_mode;
962
963            let record = discv5.try_into_reachable(&remote_enr, mapped_socket).unwrap();
964            assert_eq!(record.tcp_port, TCP4_PORT);
965            assert_eq!(record.address, mapped_socket.ip());
966            assert_eq!(record.udp_port, mapped_socket.port());
967
968            let peer = discv5.on_discovered_peer(&remote_enr, mapped_socket).unwrap();
969            assert_eq!(peer.node_record, record);
970        }
971    }
972
973    #[test]
974    fn reachable_tcp_port_v6_session_falls_back_to_tcp4() {
975        // A v6 session whose ENR carries only a `tcp` (v4) port uses that port
976        // rather than the session's UDP port.
977        const TCP4_PORT: u16 = 30304;
978        let remote_key = CombinedKey::generate_secp256k1();
979        let remote_enr = Enr::builder().tcp4(TCP4_PORT).build(&remote_key).unwrap();
980
981        let v6_socket: SocketAddr = "[::1]:30307".parse().unwrap();
982
983        let mut discv5 = discv5_noop();
984        discv5.rlpx_ip_mode = IpMode::Ip6;
985        let record = discv5.try_into_reachable(&remote_enr, v6_socket).unwrap();
986        assert_eq!(record.tcp_port, TCP4_PORT);
987    }
988
989    #[test]
990    fn reachable_tcp_port_missing_falls_back_to_udp_port() {
991        // No TCP port in the ENR at all: keep the long-standing fallback to
992        // the session's UDP port, for either session family.
993        let remote_key = CombinedKey::generate_secp256k1();
994        let remote_enr = Enr::builder().build(&remote_key).unwrap();
995
996        let discv5 = discv5_noop();
997        let v4_socket: SocketAddr = "127.0.0.1:30305".parse().unwrap();
998        assert_eq!(discv5.try_into_reachable(&remote_enr, v4_socket).unwrap().tcp_port, 30305);
999        let v6_socket: SocketAddr = "[::1]:30307".parse().unwrap();
1000        assert_eq!(discv5.try_into_reachable(&remote_enr, v6_socket).unwrap().tcp_port, 30307);
1001    }
1002
1003    // Copied from sigp/discv5 with slight modification (U256 type)
1004    // <https://github.com/sigp/discv5/blob/master/src/kbucket/key.rs#L89-L101>
1005    #[expect(unreachable_pub)]
1006    #[expect(unused)]
1007    mod sigp {
1008        use alloy_primitives::U256;
1009        use enr::{
1010            k256::sha2::digest::generic_array::{typenum::U32, GenericArray},
1011            NodeId,
1012        };
1013
1014        /// A `Key` is a cryptographic hash, identifying both the nodes participating in
1015        /// the Kademlia DHT, as well as records stored in the DHT.
1016        ///
1017        /// The set of all `Key`s defines the Kademlia keyspace.
1018        ///
1019        /// `Key`s have an XOR metric as defined in the Kademlia paper, i.e. the bitwise XOR of
1020        /// the hash digests, interpreted as an integer. See [`Key::distance`].
1021        ///
1022        /// A `Key` preserves the preimage of type `T` of the hash function. See [`Key::preimage`].
1023        #[derive(Clone, Debug)]
1024        pub struct Key<T> {
1025            preimage: T,
1026            hash: GenericArray<u8, U32>,
1027        }
1028
1029        impl<T> PartialEq for Key<T> {
1030            fn eq(&self, other: &Self) -> bool {
1031                self.hash == other.hash
1032            }
1033        }
1034
1035        impl<T> Eq for Key<T> {}
1036
1037        impl<TPeerId> AsRef<Self> for Key<TPeerId> {
1038            fn as_ref(&self) -> &Self {
1039                self
1040            }
1041        }
1042
1043        impl<T> Key<T> {
1044            /// Construct a new `Key` by providing the raw 32 byte hash.
1045            pub const fn new_raw(preimage: T, hash: GenericArray<u8, U32>) -> Self {
1046                Self { preimage, hash }
1047            }
1048
1049            /// Borrows the preimage of the key.
1050            pub const fn preimage(&self) -> &T {
1051                &self.preimage
1052            }
1053
1054            /// Converts the key into its preimage.
1055            pub fn into_preimage(self) -> T {
1056                self.preimage
1057            }
1058
1059            /// Computes the distance of the keys according to the XOR metric.
1060            pub fn distance<U>(&self, other: &Key<U>) -> Distance {
1061                let a = U256::from_be_slice(self.hash.as_slice());
1062                let b = U256::from_be_slice(other.hash.as_slice());
1063                Distance(a ^ b)
1064            }
1065
1066            // Used in the FINDNODE query outside of the k-bucket implementation.
1067            /// Computes the integer log-2 distance between two keys, assuming a 256-bit
1068            /// key. The output returns None if the key's are identical. The range is 1-256.
1069            pub fn log2_distance<U>(&self, other: &Key<U>) -> Option<u64> {
1070                let xor_dist = self.distance(other);
1071                let log_dist = (256 - xor_dist.0.leading_zeros() as u64);
1072                (log_dist != 0).then_some(log_dist)
1073            }
1074        }
1075
1076        impl From<NodeId> for Key<NodeId> {
1077            fn from(node_id: NodeId) -> Self {
1078                Self { preimage: node_id, hash: *GenericArray::from_slice(&node_id.raw()) }
1079            }
1080        }
1081
1082        /// A distance between two `Key`s.
1083        #[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Debug)]
1084        pub struct Distance(pub(super) U256);
1085    }
1086
1087    #[test]
1088    fn select_lookup_target() {
1089        for bucket_index in 0..=MAX_KBUCKET_INDEX {
1090            let sk = CombinedKey::generate_secp256k1();
1091            let local_node_id = discv5::enr::NodeId::from(sk.public());
1092            let target = get_lookup_target(bucket_index, local_node_id);
1093
1094            let local_node_id = sigp::Key::from(local_node_id);
1095            let target = sigp::Key::from(target);
1096
1097            assert_eq!(local_node_id.log2_distance(&target), Some(bucket_index as u64 + 1));
1098        }
1099    }
1100
1101    #[test]
1102    fn build_enr_from_config() {
1103        const TCP_PORT: u16 = 30303;
1104        let fork_id = MAINNET.latest_fork_id();
1105
1106        let config = Config::builder((Ipv4Addr::UNSPECIFIED, TCP_PORT).into())
1107            .fork(NetworkStackId::ETH, fork_id)
1108            .build();
1109
1110        let sk = SecretKey::new(&mut thread_rng());
1111        let (enr, _, _, _) = build_local_enr(&sk, &config);
1112
1113        let decoded_fork_id = enr
1114            .get_decodable::<EnrForkIdEntry>(NetworkStackId::ETH)
1115            .unwrap()
1116            .map(Into::into)
1117            .unwrap();
1118
1119        assert_eq!(fork_id, decoded_fork_id);
1120        assert_eq!(TCP_PORT, enr.tcp4().unwrap()); // listen config is defaulting to ip mode ipv4
1121    }
1122
1123    #[test]
1124    fn get_fork_id_with_different_network_stack_ids() {
1125        let fork_id = MAINNET.latest_fork_id();
1126        let sk = SecretKey::new(&mut thread_rng());
1127
1128        // Test 1: ENR with OPEL fork ID, Discv5 configured for OPEL
1129        let enr_with_opel = Enr::builder()
1130            .add_value_rlp(
1131                NetworkStackId::OPEL,
1132                alloy_rlp::encode(EnrForkIdEntry::from(fork_id)).into(),
1133            )
1134            .build(&sk)
1135            .unwrap();
1136
1137        let mut discv5 = discv5_noop();
1138        discv5.fork_key = Some(NetworkStackId::OPEL);
1139        assert_eq!(discv5.get_fork_id(&enr_with_opel).unwrap(), fork_id);
1140
1141        // Test 2: ENR with ETH fork ID, Discv5 configured for OPEL (fallback to ETH)
1142        let enr_with_eth = Enr::builder()
1143            .add_value_rlp(
1144                NetworkStackId::ETH,
1145                alloy_rlp::encode(EnrForkIdEntry::from(fork_id)).into(),
1146            )
1147            .build(&sk)
1148            .unwrap();
1149
1150        discv5.fork_key = Some(NetworkStackId::OPEL);
1151        assert_eq!(discv5.get_fork_id(&enr_with_eth).unwrap(), fork_id);
1152
1153        // Test 3: ENR with neither OPEL nor ETH fork ID (should fail)
1154        let enr_without_network_stack_id = Enr::empty(&sk).unwrap();
1155        discv5.fork_key = Some(NetworkStackId::OPEL);
1156        assert!(matches!(
1157            discv5.get_fork_id(&enr_without_network_stack_id),
1158            Err(Error::ForkMissing(NetworkStackId::OPEL))
1159        ));
1160
1161        // Test 4: discv5 without network stack id configured (should fail)
1162        let discv5 = discv5_noop();
1163        assert!(matches!(
1164            discv5.get_fork_id(&enr_without_network_stack_id),
1165            Err(Error::NetworkStackIdNotConfigured)
1166        ));
1167    }
1168}