Skip to main content

reth_network/
discovery.rs

1//! Discovery support for the network.
2
3use crate::{
4    cache::LruMap,
5    error::{NetworkError, ServiceKind},
6};
7use enr::Enr;
8use futures::StreamExt;
9use reth_discv4::{DiscoveryUpdate, Discv4, Discv4Config};
10use reth_discv5::{DiscoveredPeer, Discv5};
11use reth_dns_discovery::{
12    DnsDiscoveryConfig, DnsDiscoveryHandle, DnsDiscoveryService, DnsNodeRecordUpdate, DnsResolver,
13};
14use reth_ethereum_forks::{EnrForkIdEntry, ForkId};
15use reth_net_nat::{NatResolver, ResolveNatInterval};
16use reth_network_api::{DiscoveredEvent, DiscoveryEvent};
17use reth_network_peers::{NodeRecord, PeerId};
18use reth_network_types::PeerAddr;
19use secp256k1::SecretKey;
20use socket2::{Domain, Protocol, Socket, Type};
21use std::{
22    collections::VecDeque,
23    net::{IpAddr, SocketAddr},
24    pin::Pin,
25    sync::Arc,
26    task::{ready, Context, Poll},
27    time::Duration,
28};
29use tokio::{net::UdpSocket, sync::mpsc, task::JoinHandle};
30use tokio_stream::{wrappers::ReceiverStream, Stream};
31use tracing::{debug, trace};
32
33/// Default max capacity for cache of discovered peers.
34///
35/// Default is 10 000 peers.
36pub const DEFAULT_MAX_CAPACITY_DISCOVERED_PEERS_CACHE: u32 = 10_000;
37
38/// How often discv5-only nodes refresh their external IP.
39///
40/// Matches the default interval of discv4.
41const RESOLVE_EXTERNAL_IP_INTERVAL: Duration = Duration::from_secs(60 * 5);
42
43/// An abstraction over the configured discovery protocol.
44///
45/// Listens for new discovered nodes and emits events for discovered nodes and their
46/// address.
47#[derive(Debug)]
48pub struct Discovery {
49    /// All nodes discovered via discovery protocol.
50    ///
51    /// These nodes can be ephemeral and are updated via the discovery protocol.
52    discovered_nodes: LruMap<PeerId, PeerAddr>,
53    /// Local ENR of the discovery v4 service (discv5 ENR has same [`PeerId`]).
54    local_enr: NodeRecord,
55    /// Handler to interact with the Discovery v4 service
56    discv4: Option<Discv4>,
57    /// All KAD table updates from the discv4 service.
58    discv4_updates: Option<ReceiverStream<DiscoveryUpdate>>,
59    /// The handle to the spawned discv4 service
60    _discv4_service: Option<JoinHandle<()>>,
61    /// Handler to interact with the Discovery v5 service
62    discv5: Option<Discv5>,
63    /// All KAD table updates from the discv5 service.
64    discv5_updates: Option<ReceiverStream<discv5::Event>>,
65    /// Background task that, in shared-port mode, drains `UnrecognizedFrame`s from discv5 and
66    /// feeds them into the discv4 ingress so packets advance without polling `Discovery`.
67    _discv5_forwarder: Option<JoinHandle<()>>,
68    /// Handler to interact with the DNS discovery service
69    _dns_discovery: Option<DnsDiscoveryHandle>,
70    /// Updates from the DNS discovery service.
71    dns_discovery_updates: Option<ReceiverStream<DnsNodeRecordUpdate>>,
72    /// The handle to the spawned DNS discovery service
73    _dns_disc_service: Option<JoinHandle<()>>,
74    /// Events buffered until polled.
75    queued_events: VecDeque<DiscoveryEvent>,
76    /// List of listeners subscribed to discovery events.
77    discovery_listeners: Vec<mpsc::UnboundedSender<DiscoveryEvent>>,
78    /// Resolves the external IP for discv5 when discv4 is disabled.
79    nat_resolver: Option<ResolveNatInterval>,
80}
81
82impl Discovery {
83    /// Spawns the discovery service.
84    ///
85    /// This will spawn the [`reth_discv4::Discv4Service`] onto a new task and establish a listener
86    /// channel to receive all discovered nodes. When only discv5 is enabled, `nat` periodically
87    /// resolves its advertised external IP without waiting for peer votes. When ENR updates are
88    /// disabled or discv5 checks inbound connectivity, only explicit `extip` or `extaddr` settings
89    /// are applied.
90    pub async fn new(
91        tcp_addr: SocketAddr,
92        discovery_v4_addr: SocketAddr,
93        sk: SecretKey,
94        discv4_config: Option<Discv4Config>,
95        mut discv5_config: Option<reth_discv5::Config>, // contains discv5 listen address
96        dns_discovery_config: Option<DnsDiscoveryConfig>,
97        nat: Option<NatResolver>,
98    ) -> Result<Self, NetworkError> {
99        // setup discv4 with the discovery address and tcp port
100        let local_enr =
101            NodeRecord::from_secret_key(discovery_v4_addr, &sk).with_tcp_port(tcp_addr.port());
102
103        // For IPv6 we set IPV6_V6ONLY=true so an IPv4 sibling socket on the same port doesn't
104        // clash with the IPv6 one (Linux's default of V6ONLY=0 has IPv6 also claim the IPv4
105        // port via mapped addresses), matching how discv5 binds its `DualStack` sockets.
106        let bind_socket = async |addr: SocketAddr| {
107            let result = match addr {
108                SocketAddr::V4(_) => UdpSocket::bind(addr).await,
109                SocketAddr::V6(_) => (|| {
110                    let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
111                    socket.set_only_v6(true)?;
112                    socket.set_nonblocking(true)?;
113                    socket.bind(&addr.into())?;
114                    UdpSocket::from_std(socket.into())
115                })(),
116            };
117            result
118                .map(Arc::new)
119                .map_err(|err| NetworkError::from_io_error(err, ServiceKind::Discovery(addr)))
120        };
121
122        // In shared-port mode, bind the shared socket and start discv4 without its own receive
123        // loop. Unrecognized frames from discv5 will be forwarded to the ingress handler, and
124        // discv5's opposite-family sibling socket doubles as discv4's secondary-family egress.
125        // The sibling binds at the shared port that discv4 advertises for both families, not at
126        // discv5's configured opposite-family port, and serves egress and replies only. Node
127        // config has no second-family external IP source, so the discv4 ENR advertises only the
128        // discovery address family. The discv5 ENR carries the dual-stack advertisement.
129        let (discv4, discv4_updates, _discv4_service, discv4_ingress, shared_sockets) =
130            if let Some(mut config) = discv4_config {
131                if let Some(discv5_config) = &mut discv5_config &&
132                    discv5_config.has_matching_socket(discovery_v4_addr)
133                {
134                    let socket = bind_socket(discovery_v4_addr).await?;
135                    let shared_port = socket
136                        .local_addr()
137                        .map_err(|err| {
138                            NetworkError::from_io_error(
139                                err,
140                                ServiceKind::Discovery(discovery_v4_addr),
141                            )
142                        })?
143                        .port();
144
145                    let listen_config = &discv5_config.discv5_config_mut().listen_config;
146                    let secondary_addr = reth_discv5::config::sibling_addr(
147                        listen_config,
148                        discovery_v4_addr,
149                        shared_port,
150                    );
151                    let secondary_socket = match secondary_addr {
152                        Some(addr) => Some(bind_socket(addr).await?),
153                        None => None,
154                    };
155
156                    if secondary_socket.is_some() {
157                        // The TCP listener's family can differ from the primary discovery socket.
158                        let (tcp4, tcp6) = if tcp_addr.is_ipv4() {
159                            (tcp_addr.port(), 0)
160                        } else {
161                            (0, tcp_addr.port())
162                        };
163                        config.add_eip868_pair("tcp", tcp4);
164                        config.add_eip868_pair("tcp6", tcp6);
165                    }
166
167                    let (discv4, mut discv4_service, ingress) = Discv4::bind_shared(
168                        socket.clone(),
169                        secondary_socket.clone(),
170                        local_enr,
171                        sk,
172                        config,
173                    )
174                    .map_err(|err| {
175                        NetworkError::from_io_error(err, ServiceKind::Discovery(discovery_v4_addr))
176                    })?;
177
178                    let discv4_updates = discv4_service.update_stream();
179                    let discv4_service = discv4_service.spawn();
180                    debug!(target:"net", ?discovery_v4_addr, "started discovery v4 (shared port)");
181                    (
182                        Some(discv4),
183                        Some(discv4_updates),
184                        Some(discv4_service),
185                        Some(ingress),
186                        Some((socket, secondary_socket)),
187                    )
188                } else {
189                    let (discv4, mut discv4_service) =
190                        Discv4::bind(discovery_v4_addr, local_enr, sk, config).await.map_err(
191                            |err| {
192                                NetworkError::from_io_error(
193                                    err,
194                                    ServiceKind::Discovery(discovery_v4_addr),
195                                )
196                            },
197                        )?;
198                    let discv4_updates = discv4_service.update_stream();
199                    // spawn the service
200                    let discv4_service = discv4_service.spawn();
201
202                    debug!(target:"net", ?discovery_v4_addr, "started discovery v4");
203
204                    (Some(discv4), Some(discv4_updates), Some(discv4_service), None, None)
205                }
206            } else {
207                (None, None, None, None, None)
208            };
209
210        // Discv4 resolves its own external IP when enabled.
211        let nat_resolver = if discv4.is_none() &&
212            let Some(config) = &mut discv5_config
213        {
214            let config = config.discv5_config_mut();
215            nat.filter(|nat| match nat {
216                NatResolver::None => false,
217                // Use the address or hostname explicitly configured by the operator.
218                NatResolver::ExternalIp(_) | NatResolver::ExternalAddr(_) => true,
219                // Preserve manual addresses and let discv5 handle reachability checks. Its check
220                // for incoming connections starts when peer votes change the advertised address.
221                _ => config.enr_update && config.auto_nat_listen_duration.is_none(),
222            })
223            .map(|nat| ResolveNatInterval::interval(nat, RESOLVE_EXTERNAL_IP_INTERVAL))
224        } else {
225            None
226        };
227
228        // Start discv5, wiring in the shared socket if in shared-port mode.
229        let (discv5, discv5_updates) = if let Some(mut config) = discv5_config {
230            // Set OS-assigned advertised RLPx ports to the bound listener port.
231            set_bound_rlpx_port_if_unset(&mut config, tcp_addr.port());
232
233            if let Some((primary, secondary)) = shared_sockets {
234                let (ipv4, ipv6) = if discovery_v4_addr.is_ipv4() {
235                    (Some(primary), secondary)
236                } else {
237                    (secondary, Some(primary))
238                };
239                config.discv5_config_mut().listen_config =
240                    discv5::ListenConfig::FromSockets { ipv4, ipv6 };
241            }
242
243            let (discv5, discv5_updates) = Discv5::start(&sk, config).await?;
244            debug!(target:"net", discovery_v5_enr=?discv5.local_enr(), "started discovery v5");
245            (Some(discv5), Some(discv5_updates))
246        } else {
247            (None, None)
248        };
249
250        // In shared-port mode, spawn a task that peels `UnrecognizedFrame` events off the discv5
251        // update stream and feeds them into discv4's ingress. Other events are forwarded through
252        // a new channel that `Discovery::poll` reads. This keeps both protocols moving without
253        // requiring the main `Discovery::poll` loop to be driven for packets to be routed.
254        let (discv5_updates, _discv5_forwarder) = match (discv4_ingress, discv5_updates) {
255            (Some(mut ingress), Some(mut updates)) => {
256                let (tx, rx) = mpsc::channel(updates.max_capacity());
257                let handle = tokio::spawn(async move {
258                    while let Some(event) = updates.recv().await {
259                        if let discv5::Event::UnrecognizedFrame(frame) = &event {
260                            ingress.handle_packet(&frame.packet, frame.src_address).await;
261                            continue;
262                        }
263                        if tx.send(event).await.is_err() {
264                            break;
265                        }
266                    }
267                });
268                (Some(ReceiverStream::new(rx)), Some(handle))
269            }
270            (_, updates) => (updates.map(ReceiverStream::new), None),
271        };
272
273        // setup DNS discovery
274        let (_dns_discovery, dns_discovery_updates, _dns_disc_service) =
275            if let Some(dns_config) = dns_discovery_config {
276                let (mut service, dns_disc) = DnsDiscoveryService::new_pair(
277                    Arc::new(DnsResolver::from_system_conf()?),
278                    dns_config,
279                );
280                let dns_discovery_updates = service.node_record_stream();
281                let dns_disc_service = service.spawn();
282                (Some(dns_disc), Some(dns_discovery_updates), Some(dns_disc_service))
283            } else {
284                (None, None, None)
285            };
286
287        Ok(Self {
288            discovery_listeners: Default::default(),
289            local_enr,
290            discv4,
291            discv4_updates,
292            _discv4_service,
293            discv5,
294            discv5_updates,
295            _discv5_forwarder,
296            discovered_nodes: LruMap::new(DEFAULT_MAX_CAPACITY_DISCOVERED_PEERS_CACHE),
297            queued_events: Default::default(),
298            _dns_disc_service,
299            _dns_discovery,
300            dns_discovery_updates,
301            nat_resolver,
302        })
303    }
304
305    /// Registers a listener for receiving [`DiscoveryEvent`] updates.
306    pub(crate) fn add_listener(&mut self, tx: mpsc::UnboundedSender<DiscoveryEvent>) {
307        self.discovery_listeners.push(tx);
308    }
309
310    /// Notifies all registered listeners with the provided `event`.
311    #[inline]
312    fn notify_listeners(&mut self, event: &DiscoveryEvent) {
313        self.discovery_listeners.retain_mut(|listener| listener.send(event.clone()).is_ok());
314    }
315
316    /// Updates the `eth:ForkId` field in discv4/discv5.
317    pub(crate) fn update_fork_id(&self, fork_id: ForkId) {
318        if let Some(discv4) = &self.discv4 {
319            // use forward-compatible forkid entry
320            discv4.set_eip868_rlp(b"eth".to_vec(), EnrForkIdEntry::from(fork_id))
321        }
322        if let Some(discv5) = &self.discv5 {
323            discv5
324                .encode_and_set_eip868_in_local_enr(b"eth".to_vec(), EnrForkIdEntry::from(fork_id))
325        }
326    }
327
328    /// Bans the [`IpAddr`] in the discovery service.
329    pub(crate) fn ban_ip(&self, ip: IpAddr) {
330        if let Some(discv4) = &self.discv4 {
331            discv4.ban_ip(ip)
332        }
333        if let Some(discv5) = &self.discv5 {
334            discv5.ban_ip(ip)
335        }
336    }
337
338    /// Bans the [`PeerId`] and [`IpAddr`] in the discovery service.
339    pub(crate) fn ban(&self, peer_id: PeerId, ip: IpAddr) {
340        if let Some(discv4) = &self.discv4 {
341            discv4.ban(peer_id, ip)
342        }
343        if let Some(discv5) = &self.discv5 {
344            discv5.ban(peer_id, ip)
345        }
346    }
347
348    /// Returns a shared reference to the discv4.
349    pub fn discv4(&self) -> Option<Discv4> {
350        self.discv4.clone()
351    }
352
353    /// Returns the id with which the local node identifies itself in the network
354    pub(crate) const fn local_id(&self) -> PeerId {
355        self.local_enr.id // local discv4 and discv5 have same id, since signed with same secret key
356    }
357
358    /// Add a node to the discv4 table.
359    pub(crate) fn add_discv4_node(&self, node: NodeRecord) {
360        if let Some(discv4) = &self.discv4 {
361            discv4.add_node(node);
362        }
363    }
364
365    /// Returns discv5 handle.
366    pub fn discv5(&self) -> Option<Discv5> {
367        self.discv5.clone()
368    }
369
370    /// Add a node to the discv4 table.
371    pub(crate) fn add_discv5_node(&self, enr: Enr<SecretKey>) -> Result<(), NetworkError> {
372        if let Some(discv5) = &self.discv5 {
373            discv5.add_node(enr).map_err(NetworkError::Discv5Error)?;
374        }
375
376        Ok(())
377    }
378
379    /// Processes an incoming [`NodeRecord`] update from a discovery service
380    fn on_node_record_update(&mut self, record: NodeRecord, fork_id: Option<ForkId>) {
381        let peer_id = record.id;
382        let tcp_addr = record.tcp_addr();
383        if tcp_addr.port() == 0 {
384            // useless peer for p2p
385            return
386        }
387        let udp_addr = record.udp_addr();
388        let addr = PeerAddr::new(tcp_addr, Some(udp_addr));
389        _ =
390            self.discovered_nodes.get_or_insert(peer_id, || {
391                self.queued_events.push_back(DiscoveryEvent::NewNode(
392                    DiscoveredEvent::EventQueued { peer_id, addr, fork_id },
393                ));
394
395                addr
396            })
397    }
398
399    fn on_discv4_update(&mut self, update: DiscoveryUpdate) {
400        match update {
401            DiscoveryUpdate::Added(record) | DiscoveryUpdate::DiscoveredAtCapacity(record) => {
402                self.on_node_record_update(record, None);
403            }
404            DiscoveryUpdate::EnrForkId(node, fork_id) => {
405                self.queued_events.push_back(DiscoveryEvent::EnrForkId(node, fork_id))
406            }
407            DiscoveryUpdate::Removed(peer_id) => {
408                self.discovered_nodes.remove(&peer_id);
409            }
410            DiscoveryUpdate::Batch(updates) => {
411                for update in updates {
412                    self.on_discv4_update(update);
413                }
414            }
415        }
416    }
417
418    pub(crate) fn poll(&mut self, cx: &mut Context<'_>) -> Poll<DiscoveryEvent> {
419        loop {
420            // Drain all buffered events first
421            if let Some(event) = self.queued_events.pop_front() {
422                self.notify_listeners(&event);
423                return Poll::Ready(event)
424            }
425
426            // drain the discv4 update stream
427            while let Some(Poll::Ready(Some(update))) =
428                self.discv4_updates.as_mut().map(|updates| updates.poll_next_unpin(cx))
429            {
430                self.on_discv4_update(update)
431            }
432
433            // drain the discv5 update stream
434            while let Some(Poll::Ready(Some(update))) =
435                self.discv5_updates.as_mut().map(|updates| updates.poll_next_unpin(cx))
436            {
437                if let Some(discv5) = self.discv5.as_mut() &&
438                    let Some(DiscoveredPeer { node_record, fork_id }) =
439                        discv5.on_discv5_update(update)
440                {
441                    self.on_node_record_update(node_record, fork_id);
442                }
443            }
444
445            // drain the dns update stream
446            while let Some(Poll::Ready(Some(update))) =
447                self.dns_discovery_updates.as_mut().map(|updates| updates.poll_next_unpin(cx))
448            {
449                self.add_discv4_node(update.node_record);
450                if let Err(err) = self.add_discv5_node(update.enr) {
451                    trace!(target: "net::discovery",
452                        %err,
453                        "failed adding node discovered by dns to discv5"
454                    );
455                }
456                self.on_node_record_update(update.node_record, update.fork_id);
457            }
458
459            while let Some(Poll::Ready(ip)) =
460                self.nat_resolver.as_mut().map(|resolver| resolver.poll_tick(cx))
461            {
462                if let Some(ip) = ip {
463                    self.on_external_ip(ip);
464                }
465            }
466
467            if self.queued_events.is_empty() {
468                return Poll::Pending
469            }
470        }
471    }
472
473    /// Updates discv5's advertised IP without changing ports or address families.
474    ///
475    /// Discv4 owns its NAT resolution and updates its own node record and ENR, so only
476    /// discv5 needs an update here.
477    fn on_external_ip(&self, external_ip: IpAddr) {
478        let Some(discv5) = &self.discv5 else { return };
479        let enr = discv5.local_enr();
480        // TCP and UDP share the ENR IP fields. Preserve their ports, including ports learned
481        // from peer votes, and only update address families the service is listening on.
482        let result = match external_ip {
483            IpAddr::V4(ip) if enr.udp4().is_some() && enr.ip4() != Some(ip) => {
484                discv5.with_discv5(|discv5| discv5.enr_insert("ip", &ip))
485            }
486            IpAddr::V6(ip) if enr.udp6().is_some() && enr.ip6() != Some(ip) => {
487                discv5.with_discv5(|discv5| discv5.enr_insert("ip6", &ip))
488            }
489            _ => return,
490        };
491        match result {
492            Ok(_) => debug!(target: "net::discovery", ?external_ip, "Updated external IP"),
493            Err(err) => {
494                debug!(target: "net::discovery", ?external_ip, %err, "Failed to update external IP");
495            }
496        }
497    }
498}
499
500const fn set_bound_rlpx_port_if_unset(config: &mut reth_discv5::Config, port: u16) {
501    if config.rlpx_socket().port() == 0 {
502        config.set_rlpx_port(port);
503    }
504}
505
506impl Drop for Discovery {
507    fn drop(&mut self) {
508        if let Some(discv4) = &self.discv4 {
509            discv4.terminate();
510        }
511        if let Some(handle) = self._discv4_service.take() {
512            handle.abort();
513        }
514        if let Some(handle) = self._discv5_forwarder.take() {
515            handle.abort();
516        }
517        if let Some(handle) = self._dns_disc_service.take() {
518            handle.abort();
519        }
520    }
521}
522
523impl Stream for Discovery {
524    type Item = DiscoveryEvent;
525
526    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
527        Poll::Ready(Some(ready!(self.get_mut().poll(cx))))
528    }
529}
530
531#[cfg(test)]
532impl Discovery {
533    /// Returns a Discovery instance that does nothing and is intended for testing purposes.
534    ///
535    /// NOTE: This instance does nothing
536    pub(crate) fn noop() -> Self {
537        let (_discovery_listeners, _): (mpsc::UnboundedSender<DiscoveryEvent>, _) =
538            mpsc::unbounded_channel();
539
540        Self {
541            discovered_nodes: LruMap::new(0),
542            local_enr: NodeRecord {
543                address: IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
544                tcp_port: 0,
545                udp_port: 0,
546                id: PeerId::random(),
547            },
548            discv4: Default::default(),
549            discv4_updates: Default::default(),
550            _discv4_service: Default::default(),
551            _discv5_forwarder: None,
552            discv5: None,
553            discv5_updates: None,
554            queued_events: Default::default(),
555            _dns_discovery: None,
556            dns_discovery_updates: None,
557            _dns_disc_service: None,
558            discovery_listeners: Default::default(),
559            nat_resolver: None,
560        }
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use secp256k1::SECP256K1;
568    use std::{
569        net::{Ipv4Addr, Ipv6Addr, SocketAddrV4},
570        time::Duration,
571    };
572
573    #[tokio::test(flavor = "multi_thread")]
574    async fn test_discovery_setup() {
575        let (secret_key, _) = SECP256K1.generate_keypair(&mut rand_08::thread_rng());
576        let discovery_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0));
577        let _discovery = Discovery::new(
578            discovery_addr,
579            discovery_addr,
580            secret_key,
581            Default::default(),
582            None,
583            Default::default(),
584            None,
585        )
586        .await
587        .unwrap();
588    }
589
590    use reth_discv4::Discv4ConfigBuilder;
591    use reth_discv5::{enr::EnrCombinedKeyWrapper, enr_to_discv4_id};
592    use tracing::trace;
593
594    async fn start_discovery_node(udp_port_discv4: u16, udp_port_discv5: u16) -> Discovery {
595        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
596
597        let discv4_addr = format!("127.0.0.1:{udp_port_discv4}").parse().unwrap();
598        let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
599
600        // disable `NatResolver`
601        let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
602
603        let discv5_listen_config = discv5::ListenConfig::from(discv5_addr);
604        let discv5_config = reth_discv5::Config::builder(discv5_addr)
605            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
606            .build();
607
608        Discovery::new(
609            discv4_addr,
610            discv4_addr,
611            secret_key,
612            Some(discv4_config),
613            Some(discv5_config),
614            None,
615            None,
616        )
617        .await
618        .expect("should build discv5 with discv4 downgrade")
619    }
620
621    async fn start_discv5_only(config: reth_discv5::Config, nat: Option<NatResolver>) -> Discovery {
622        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
623        Discovery::new(
624            "127.0.0.1:30303".parse().unwrap(),
625            "127.0.0.1:0".parse().unwrap(),
626            secret_key,
627            None,
628            Some(config),
629            None,
630            nat,
631        )
632        .await
633        .expect("should start discv5")
634    }
635
636    #[tokio::test]
637    async fn discv5_only_advertises_resolved_external_ip() {
638        let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap());
639        let udp_port = socket.local_addr().unwrap().port();
640        let external_ip = "203.0.113.7".parse().unwrap();
641        // The advertised TCP port may differ from the listener port behind NAT.
642        let mut config = reth_discv5::Config::builder("127.0.0.1:30309".parse().unwrap()).build();
643        // Install pre-bound sockets after the builder normalizes its listen addresses.
644        let discv5_config = config.discv5_config_mut();
645        discv5_config.listen_config =
646            discv5::ListenConfig::FromSockets { ipv4: Some(socket), ipv6: None };
647        // Explicit NAT addresses still apply when peer-driven ENR updates are disabled.
648        discv5_config.enr_update = false;
649        let mut discovery =
650            start_discv5_only(config, Some(NatResolver::ExternalIp(external_ip))).await;
651        let discv5 = discovery.discv5().unwrap();
652        assert!(discv5.local_enr().ip4().is_none());
653
654        tokio::time::timeout(
655            Duration::from_secs(5),
656            futures::future::poll_fn(|cx| {
657                let _ = discovery.poll(cx);
658                if discv5.node_record().is_some_and(|record| record.address == external_ip) {
659                    Poll::Ready(())
660                } else {
661                    Poll::Pending
662                }
663            }),
664        )
665        .await
666        .expect("external IP should be advertised");
667
668        let record = discv5.node_record().unwrap();
669        assert_eq!(record.address, external_ip);
670        assert_eq!(record.udp_port, udp_port);
671        assert_eq!(record.tcp_port, 30309);
672
673        let seq = discv5.local_enr().seq();
674        discovery.on_external_ip(external_ip);
675        assert_eq!(discv5.local_enr().seq(), seq);
676        discovery.on_external_ip("2001:db8::7".parse().unwrap());
677        assert_eq!(discv5.local_enr().seq(), seq);
678
679        // Peer votes can change the address and mapped UDP port between NAT resolutions.
680        discv5.with_discv5(|discv5| {
681            discv5.update_local_enr_socket((Ipv4Addr::LOCALHOST, 30310).into(), false);
682        });
683        discovery.on_external_ip(external_ip);
684        assert_eq!(discv5.node_record().unwrap(), NodeRecord { udp_port: 30310, ..record });
685    }
686
687    #[tokio::test]
688    async fn discv5_only_respects_enr_update_policy() {
689        for (enr_update, auto_nat_listen_duration) in
690            [(false, None), (true, Some(Duration::from_secs(300)))]
691        {
692            let mut config = reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 0).into()).build();
693            let discv5_config = config.discv5_config_mut();
694            discv5_config.listen_config =
695                discv5::ListenConfig::Ipv4 { ip: Ipv4Addr::LOCALHOST, port: 0 };
696            discv5_config.enr_update = enr_update;
697            // Set this after building: the upstream builder clears the reachability timer.
698            discv5_config.auto_nat_listen_duration = auto_nat_listen_duration;
699            let discovery = start_discv5_only(config, Some(NatResolver::Any)).await;
700
701            // No automatic lookup may overwrite a pinned address or bypass reachability checks.
702            assert!(discovery.nat_resolver.is_none());
703        }
704    }
705
706    #[test]
707    fn discv5_enr_advertises_bound_rlpx_port() {
708        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
709
710        let bound_rlpx_port = 30307;
711        let discv5_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
712        let mut discv5_config = reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 0).into())
713            .discv5_config(discv5::ConfigBuilder::new(discv5_addr.into()).build())
714            .build();
715
716        set_bound_rlpx_port_if_unset(&mut discv5_config, bound_rlpx_port);
717
718        let (enr, _, _, _) = reth_discv5::build_local_enr(&secret_key, &discv5_config);
719        assert_eq!(enr.tcp4(), Some(bound_rlpx_port));
720    }
721
722    #[test]
723    fn discv5_enr_preserves_configured_rlpx_port() {
724        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
725
726        let advertised_addr: SocketAddr = "127.0.0.1:30308".parse().unwrap();
727        let discv5_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
728        let mut discv5_config = reth_discv5::Config::builder(advertised_addr)
729            .discv5_config(discv5::ConfigBuilder::new(discv5_addr.into()).build())
730            .build();
731
732        set_bound_rlpx_port_if_unset(&mut discv5_config, 30307);
733
734        let (enr, _, _, _) = reth_discv5::build_local_enr(&secret_key, &discv5_config);
735        assert_eq!(enr.tcp4(), Some(advertised_addr.port()));
736    }
737
738    #[tokio::test(flavor = "multi_thread")]
739    async fn discv5_and_discv4_same_pk() {
740        reth_tracing::init_test_tracing();
741
742        // set up test
743        let mut node_1 = start_discovery_node(40014, 40015).await;
744        let discv4_enr_1 = node_1.discv4.as_ref().unwrap().node_record();
745        let discv5_enr_node_1 =
746            node_1.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
747        let discv4_id_1 = discv4_enr_1.id;
748        let discv5_id_1 = discv5_enr_node_1.node_id();
749
750        let mut node_2 = start_discovery_node(40024, 40025).await;
751        let discv4_enr_2 = node_2.discv4.as_ref().unwrap().node_record();
752        let discv5_enr_node_2 =
753            node_2.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
754        let discv4_id_2 = discv4_enr_2.id;
755        let discv5_id_2 = discv5_enr_node_2.node_id();
756
757        trace!(target: "net::discovery::tests",
758            node_1_node_id=format!("{:#}", discv5_id_1),
759            node_2_node_id=format!("{:#}", discv5_id_2),
760            "started nodes"
761        );
762
763        // test
764
765        // assert discovery version 4 and version 5 nodes have same id
766        assert_eq!(discv4_id_1, enr_to_discv4_id(&discv5_enr_node_1).unwrap());
767        assert_eq!(discv4_id_2, enr_to_discv4_id(&discv5_enr_node_2).unwrap());
768
769        // add node_2:discv4 manually to node_1:discv4
770        node_1.add_discv4_node(discv4_enr_2);
771
772        // verify node_2:discv4 discovered node_1:discv4 and vv
773        let event_node_1 = node_1.next().await.unwrap();
774        let event_node_2 = node_2.next().await.unwrap();
775
776        assert_eq!(
777            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
778                peer_id: discv4_id_2,
779                addr: PeerAddr::new(discv4_enr_2.tcp_addr(), Some(discv4_enr_2.udp_addr())),
780                fork_id: None
781            }),
782            event_node_1
783        );
784        assert_eq!(
785            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
786                peer_id: discv4_id_1,
787                addr: PeerAddr::new(discv4_enr_1.tcp_addr(), Some(discv4_enr_1.udp_addr())),
788                fork_id: None
789            }),
790            event_node_2
791        );
792
793        assert_eq!(1, node_1.discovered_nodes.len());
794        assert_eq!(1, node_2.discovered_nodes.len());
795
796        // add node_2:discv5 to node_1:discv5, manual insertion won't emit an event
797        node_1.add_discv5_node(EnrCombinedKeyWrapper(discv5_enr_node_2.clone()).into()).unwrap();
798        // verify node_2 is in KBuckets of node_1:discv5
799        assert!(node_1
800            .discv5
801            .as_ref()
802            .unwrap()
803            .with_discv5(|discv5| discv5.table_entries_id().contains(&discv5_id_2)));
804
805        // manually trigger connection from node_1:discv5 to node_2:discv5
806        node_1
807            .discv5
808            .as_ref()
809            .unwrap()
810            .with_discv5(|discv5| discv5.send_ping(discv5_enr_node_2.clone()))
811            .await
812            .unwrap();
813
814        // this won't emit an event, since the nodes already discovered each other on discv4, the
815        // number of nodes stored for each node on this level remains 1.
816        assert_eq!(1, node_1.discovered_nodes.len());
817        assert_eq!(1, node_2.discovered_nodes.len());
818    }
819
820    /// Starts a discovery node with discv4 and discv5 sharing the same UDP port.
821    async fn start_shared_port_node(port: u16) -> Discovery {
822        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
823        let disc_addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
824        // Use a non-zero TCP port so the node record isn't filtered out by
825        // `on_node_record_update` (which drops peers with tcp port == 0).
826        let tcp_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
827
828        let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
829
830        let discv5_listen_config = discv5::ListenConfig::from(disc_addr);
831        let discv5_config = reth_discv5::Config::builder(tcp_addr)
832            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
833            .build();
834
835        // Both protocols use the same address, triggering shared-port mode
836        Discovery::new(
837            tcp_addr,
838            disc_addr,
839            secret_key,
840            Some(discv4_config),
841            Some(discv5_config),
842            None,
843            None,
844        )
845        .await
846        .expect("should start with shared port")
847    }
848
849    #[tokio::test(flavor = "multi_thread")]
850    async fn test_shared_port_setup() {
851        reth_tracing::init_test_tracing();
852
853        // Use port 0 so the OS picks a free port
854        let node = start_shared_port_node(0).await;
855
856        // Both protocols should be active
857        assert!(node.discv4.is_some(), "discv4 should be running");
858        assert!(node.discv5.is_some(), "discv5 should be running");
859    }
860
861    #[tokio::test(flavor = "multi_thread")]
862    async fn test_shared_port_discv5_discovery() {
863        reth_tracing::init_test_tracing();
864
865        let mut node_1 = start_shared_port_node(0).await;
866        let mut node_2 = start_shared_port_node(0).await;
867
868        let discv5_enr_1 = node_1.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
869        let discv5_enr_2 = node_2.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
870
871        let peer_id_1 = enr_to_discv4_id(&discv5_enr_1).unwrap();
872        let peer_id_2 = enr_to_discv4_id(&discv5_enr_2).unwrap();
873
874        // Add node_2's ENR to node_1's discv5 kbuckets and trigger a ping to establish a session.
875        // send_ping awaits the PONG, so the handshake completes before we poll the Discovery
876        // stream. The discv5 service runs its own background task.
877        node_1.add_discv5_node(EnrCombinedKeyWrapper(discv5_enr_2.clone()).into()).unwrap();
878        node_1
879            .discv5
880            .as_ref()
881            .unwrap()
882            .with_discv5(|discv5| discv5.send_ping(discv5_enr_2))
883            .await
884            .unwrap();
885
886        // Both SessionEstablished events should now be buffered in the update channels.
887        // Drive both nodes concurrently to collect them.
888        let mut event_1 = None;
889        let mut event_2 = None;
890        let timeout = tokio::time::sleep(std::time::Duration::from_secs(5));
891        tokio::pin!(timeout);
892        loop {
893            tokio::select! {
894                ev = node_1.next(), if event_1.is_none() => {
895                    event_1 = ev;
896                }
897                ev = node_2.next(), if event_2.is_none() => {
898                    event_2 = ev;
899                }
900                _ = &mut timeout => {
901                    panic!("timed out waiting for discv5 discovery events");
902                }
903            }
904            if event_1.is_some() && event_2.is_some() {
905                break;
906            }
907        }
908
909        assert!(matches!(
910            event_1.unwrap(),
911            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued { peer_id, .. })
912                if peer_id == peer_id_2
913        ));
914        assert!(matches!(
915            event_2.unwrap(),
916            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued { peer_id, .. })
917                if peer_id == peer_id_1
918        ));
919    }
920
921    #[tokio::test(flavor = "multi_thread")]
922    async fn test_shared_port_discv4_discovery() {
923        reth_tracing::init_test_tracing();
924
925        let mut node_1 = start_shared_port_node(0).await;
926        let mut node_2 = start_shared_port_node(0).await;
927
928        let enr_1 = node_1.discv4.as_ref().unwrap().node_record();
929        let enr_2 = node_2.discv4.as_ref().unwrap().node_record();
930
931        // Introduce node_2 to node_1 via discv4
932        node_1.add_discv4_node(enr_2);
933
934        // Both nodes should discover each other via discv4 ping/pong
935        let event_1 = node_1.next().await.unwrap();
936        let event_2 = node_2.next().await.unwrap();
937
938        assert_eq!(
939            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
940                peer_id: enr_2.id,
941                addr: PeerAddr::new(enr_2.tcp_addr(), Some(enr_2.udp_addr())),
942                fork_id: None
943            }),
944            event_1
945        );
946        assert_eq!(
947            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
948                peer_id: enr_1.id,
949                addr: PeerAddr::new(enr_1.tcp_addr(), Some(enr_1.udp_addr())),
950                fork_id: None
951            }),
952            event_2
953        );
954    }
955
956    /// Verifies that shared-port mode binds correctly when discv5 is configured for dual-stack.
957    /// On Linux this exercises the IPv6 V6ONLY path: without it, the IPv4 sibling would clash
958    /// with the IPv6 socket bound to the same port.
959    #[tokio::test(flavor = "multi_thread")]
960    async fn test_shared_port_dual_stack() {
961        reth_tracing::init_test_tracing();
962
963        // Find a port that's free on the v4 wildcard so we can use it for both v4 and v6.
964        let probe = UdpSocket::bind("0.0.0.0:0").await.expect("probe bind");
965        let port = probe.local_addr().unwrap().port();
966        drop(probe);
967
968        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
969        let v4_addr: SocketAddr = format!("0.0.0.0:{port}").parse().unwrap();
970        let tcp_addr: SocketAddr = "0.0.0.0:30303".parse().unwrap();
971
972        let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
973
974        let discv5_listen_config = discv5::ListenConfig::DualStack {
975            ipv4: std::net::Ipv4Addr::UNSPECIFIED,
976            ipv4_port: port,
977            ipv6: std::net::Ipv6Addr::UNSPECIFIED,
978            ipv6_port: port,
979        };
980        let discv5_config = reth_discv5::Config::builder(tcp_addr)
981            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
982            .build();
983
984        Discovery::new(
985            tcp_addr,
986            v4_addr,
987            secret_key,
988            Some(discv4_config),
989            Some(discv5_config),
990            None,
991            None,
992        )
993        .await
994        .expect("discovery should start with shared port + dual-stack");
995    }
996
997    /// A discv4 ping over the opposite family must complete the endpoint proof: the sibling
998    /// socket bound for discv5's dual-stack listen config is discv4's secondary-family egress.
999    #[tokio::test(flavor = "multi_thread")]
1000    async fn test_shared_port_dual_stack_answers_opposite_family_discv4() {
1001        reth_tracing::init_test_tracing();
1002
1003        let Ok(probe6) = UdpSocket::bind("[::1]:0").await else {
1004            // No IPv6 loopback on this host.
1005            return;
1006        };
1007        drop(probe6);
1008
1009        let ips = [IpAddr::V4(Ipv4Addr::LOCALHOST), IpAddr::V6(Ipv6Addr::LOCALHOST)];
1010        for primary_ip in ips {
1011            for tcp_ip in ips {
1012                assert_shared_port_dual_stack(primary_ip, tcp_ip).await;
1013            }
1014        }
1015    }
1016
1017    async fn assert_shared_port_dual_stack(primary_ip: IpAddr, tcp_ip: IpAddr) {
1018        let secondary_ip = if primary_ip.is_ipv4() {
1019            IpAddr::V6(Ipv6Addr::LOCALHOST)
1020        } else {
1021            IpAddr::V4(Ipv4Addr::LOCALHOST)
1022        };
1023        let probe = UdpSocket::bind((primary_ip, 0)).await.expect("probe bind");
1024        let port = probe.local_addr().unwrap().port();
1025        drop(probe);
1026
1027        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
1028        let node_id = reth_network_peers::pk2id(&secret_key.public_key(SECP256K1));
1029        let discovery_addr = SocketAddr::new(primary_ip, port);
1030        let tcp_addr = SocketAddr::new(tcp_ip, 30303);
1031
1032        let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
1033        // The sibling must use discv4's shared port despite a different configured port.
1034        let other_port = if port == u16::MAX { port - 1 } else { port + 1 };
1035        let discv5_listen_config = discv5::ListenConfig::DualStack {
1036            ipv4: Ipv4Addr::LOCALHOST,
1037            ipv4_port: if primary_ip.is_ipv4() { port } else { other_port },
1038            ipv6: Ipv6Addr::LOCALHOST,
1039            ipv6_port: if primary_ip.is_ipv6() { port } else { other_port },
1040        };
1041        let discv5_config = reth_discv5::Config::builder(tcp_addr)
1042            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
1043            .build();
1044
1045        let _node = Discovery::new(
1046            tcp_addr,
1047            discovery_addr,
1048            secret_key,
1049            Some(discv4_config),
1050            Some(discv5_config),
1051            None,
1052            None,
1053        )
1054        .await
1055        .expect("discovery should start with shared port + dual-stack");
1056
1057        // Seed the discv4-only prober with the secondary endpoint.
1058        let probe_sk = SecretKey::new(&mut rand_08::thread_rng());
1059        let secondary_node =
1060            NodeRecord { address: secondary_ip, udp_port: port, tcp_port: port, id: node_id };
1061        let probe_config = Discv4ConfigBuilder::default()
1062            .external_ip_resolver(None)
1063            .add_boot_node(secondary_node)
1064            .build();
1065        let probe_addr = SocketAddr::new(secondary_ip, 0);
1066        let probe_enr = NodeRecord::from_secret_key(probe_addr, &probe_sk);
1067        let (_probe, mut probe_service) =
1068            Discv4::bind(probe_addr, probe_enr, probe_sk, probe_config).await.unwrap();
1069        let mut updates = probe_service.update_stream();
1070        probe_service.spawn();
1071
1072        // The bootnode only becomes `Added` once its pong arrives over the secondary socket.
1073        let added = tokio::time::timeout(Duration::from_secs(5), async {
1074            while let Some(update) = updates.next().await {
1075                if let DiscoveryUpdate::Added(record) = update &&
1076                    record.id == node_id
1077                {
1078                    return true
1079                }
1080            }
1081            false
1082        })
1083        .await
1084        .unwrap_or(false);
1085        assert!(added, "node should answer discv4 over its secondary family");
1086
1087        for peer_ip in [primary_ip, secondary_ip] {
1088            let node = NodeRecord { address: peer_ip, ..secondary_node };
1089            let socket = UdpSocket::bind((peer_ip, 0)).await.unwrap();
1090            let secret_key = SecretKey::new(&mut rand_08::thread_rng());
1091            let record = NodeRecord::from_secret_key(socket.local_addr().unwrap(), &secret_key);
1092            let ping = reth_discv4::proto::Message::Ping(reth_discv4::proto::Ping {
1093                from: record.into(),
1094                to: node.into(),
1095                expire: u32::MAX.into(),
1096                enr_sq: None,
1097            });
1098            let (packet, _) = ping.encode(&secret_key);
1099            socket.send_to(&packet, node.udp_addr()).await.unwrap();
1100            let ping = tokio::time::timeout(Duration::from_secs(5), async {
1101                let mut buf = [0u8; 1280];
1102                loop {
1103                    let (len, _) = socket.recv_from(&mut buf).await.unwrap();
1104                    if let reth_discv4::proto::Message::Ping(ping) =
1105                        reth_discv4::proto::Message::decode(&buf[..len]).unwrap().msg
1106                    {
1107                        break ping
1108                    }
1109                }
1110            })
1111            .await
1112            .expect("node should ping the peer");
1113            assert_eq!(
1114                ping.from.tcp_port,
1115                if peer_ip == tcp_ip { tcp_addr.port() } else { 0 },
1116                "primary discovery IP: {primary_ip}, TCP IP: {tcp_ip}, peer IP: {peer_ip}"
1117            );
1118        }
1119    }
1120}