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_network_api::{DiscoveredEvent, DiscoveryEvent};
16use reth_network_peers::{NodeRecord, PeerId};
17use reth_network_types::PeerAddr;
18use secp256k1::SecretKey;
19use std::{
20    collections::VecDeque,
21    net::{IpAddr, SocketAddr},
22    pin::Pin,
23    sync::Arc,
24    task::{ready, Context, Poll},
25};
26use tokio::{net::UdpSocket, sync::mpsc, task::JoinHandle};
27use tokio_stream::{wrappers::ReceiverStream, Stream};
28use tracing::{debug, trace};
29
30/// Default max capacity for cache of discovered peers.
31///
32/// Default is 10 000 peers.
33pub const DEFAULT_MAX_CAPACITY_DISCOVERED_PEERS_CACHE: u32 = 10_000;
34
35/// An abstraction over the configured discovery protocol.
36///
37/// Listens for new discovered nodes and emits events for discovered nodes and their
38/// address.
39#[derive(Debug)]
40pub struct Discovery {
41    /// All nodes discovered via discovery protocol.
42    ///
43    /// These nodes can be ephemeral and are updated via the discovery protocol.
44    discovered_nodes: LruMap<PeerId, PeerAddr>,
45    /// Local ENR of the discovery v4 service (discv5 ENR has same [`PeerId`]).
46    local_enr: NodeRecord,
47    /// Handler to interact with the Discovery v4 service
48    discv4: Option<Discv4>,
49    /// All KAD table updates from the discv4 service.
50    discv4_updates: Option<ReceiverStream<DiscoveryUpdate>>,
51    /// The handle to the spawned discv4 service
52    _discv4_service: Option<JoinHandle<()>>,
53    /// Handler to interact with the Discovery v5 service
54    discv5: Option<Discv5>,
55    /// All KAD table updates from the discv5 service.
56    discv5_updates: Option<ReceiverStream<discv5::Event>>,
57    /// Background task that, in shared-port mode, drains `UnrecognizedFrame`s from discv5 and
58    /// feeds them into the discv4 ingress so packets advance without polling `Discovery`.
59    _discv5_forwarder: Option<JoinHandle<()>>,
60    /// Handler to interact with the DNS discovery service
61    _dns_discovery: Option<DnsDiscoveryHandle>,
62    /// Updates from the DNS discovery service.
63    dns_discovery_updates: Option<ReceiverStream<DnsNodeRecordUpdate>>,
64    /// The handle to the spawned DNS discovery service
65    _dns_disc_service: Option<JoinHandle<()>>,
66    /// Events buffered until polled.
67    queued_events: VecDeque<DiscoveryEvent>,
68    /// List of listeners subscribed to discovery events.
69    discovery_listeners: Vec<mpsc::UnboundedSender<DiscoveryEvent>>,
70}
71
72impl Discovery {
73    /// Spawns the discovery service.
74    ///
75    /// This will spawn the [`reth_discv4::Discv4Service`] onto a new task and establish a listener
76    /// channel to receive all discovered nodes.
77    pub async fn new(
78        tcp_addr: SocketAddr,
79        discovery_v4_addr: SocketAddr,
80        sk: SecretKey,
81        discv4_config: Option<Discv4Config>,
82        mut discv5_config: Option<reth_discv5::Config>, // contains discv5 listen address
83        dns_discovery_config: Option<DnsDiscoveryConfig>,
84    ) -> Result<Self, NetworkError> {
85        // setup discv4 with the discovery address and tcp port
86        let local_enr =
87            NodeRecord::from_secret_key(discovery_v4_addr, &sk).with_tcp_port(tcp_addr.port());
88
89        // For IPv6 we set IPV6_V6ONLY=true so an IPv4 sibling socket on the same port doesn't
90        // clash with the IPv6 one (Linux's default of V6ONLY=0 has IPv6 also claim the IPv4
91        // port via mapped addresses), matching how discv5 binds its `DualStack` sockets.
92        let bind_socket = async |addr: SocketAddr| {
93            let result = match addr {
94                SocketAddr::V4(_) => UdpSocket::bind(addr).await,
95                SocketAddr::V6(_) => {
96                    use socket2::{Domain, Protocol, Socket, Type};
97                    (|| {
98                        let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
99                        socket.set_only_v6(true)?;
100                        socket.set_nonblocking(true)?;
101                        socket.bind(&addr.into())?;
102                        UdpSocket::from_std(socket.into())
103                    })()
104                }
105            };
106            result
107                .map(Arc::new)
108                .map_err(|err| NetworkError::from_io_error(err, ServiceKind::Discovery(addr)))
109        };
110
111        // In shared-port mode, bind the shared socket and start discv4 without its own receive
112        // loop. Unrecognized frames from discv5 will be forwarded to the ingress handler.
113        let (discv4, discv4_updates, _discv4_service, discv4_ingress, shared_socket) =
114            if let Some(config) = discv4_config {
115                if let Some(discv5_config) = &mut discv5_config &&
116                    discv5_config.has_matching_socket(discovery_v4_addr)
117                {
118                    let socket = bind_socket(discovery_v4_addr).await?;
119
120                    let (discv4, mut discv4_service, ingress) = Discv4::bind_shared(
121                        socket.clone(),
122                        local_enr,
123                        sk,
124                        config,
125                    )
126                    .map_err(|err| {
127                        NetworkError::from_io_error(err, ServiceKind::Discovery(discovery_v4_addr))
128                    })?;
129
130                    let discv4_updates = discv4_service.update_stream();
131                    let discv4_service = discv4_service.spawn();
132                    debug!(target:"net", ?discovery_v4_addr, "started discovery v4 (shared port)");
133                    (
134                        Some(discv4),
135                        Some(discv4_updates),
136                        Some(discv4_service),
137                        Some(ingress),
138                        Some(socket),
139                    )
140                } else {
141                    let (discv4, mut discv4_service) =
142                        Discv4::bind(discovery_v4_addr, local_enr, sk, config).await.map_err(
143                            |err| {
144                                NetworkError::from_io_error(
145                                    err,
146                                    ServiceKind::Discovery(discovery_v4_addr),
147                                )
148                            },
149                        )?;
150                    let discv4_updates = discv4_service.update_stream();
151                    // spawn the service
152                    let discv4_service = discv4_service.spawn();
153
154                    debug!(target:"net", ?discovery_v4_addr, "started discovery v4");
155
156                    (Some(discv4), Some(discv4_updates), Some(discv4_service), None, None)
157                }
158            } else {
159                (None, None, None, None, None)
160            };
161
162        // Start discv5, wiring in the shared socket if in shared-port mode.
163        let (discv5, discv5_updates) = if let Some(mut config) = discv5_config {
164            // Set OS-assigned advertised RLPx ports to the bound listener port.
165            set_bound_rlpx_port_if_unset(&mut config, tcp_addr.port());
166
167            if let Some(socket) = shared_socket {
168                let discv5_cfg = config.discv5_config_mut();
169
170                // The shared socket covers discv4's address family; bind the opposite family
171                // only if discv5 was configured for dual-stack.
172                let (mut ipv4, mut ipv6) = (None, None);
173                if discovery_v4_addr.is_ipv4() {
174                    ipv4 = Some(socket);
175                    if let Some(addr) = reth_discv5::config::ipv6(&discv5_cfg.listen_config) {
176                        ipv6 = Some(bind_socket(SocketAddr::V6(addr)).await?);
177                    }
178                } else {
179                    ipv6 = Some(socket);
180                    if let Some(addr) = reth_discv5::config::ipv4(&discv5_cfg.listen_config) {
181                        ipv4 = Some(bind_socket(SocketAddr::V4(addr)).await?);
182                    }
183                }
184
185                discv5_cfg.listen_config = discv5::ListenConfig::FromSockets { ipv4, ipv6 };
186            }
187
188            let (discv5, discv5_updates) = Discv5::start(&sk, config).await?;
189            debug!(target:"net", discovery_v5_enr=?discv5.local_enr(), "started discovery v5");
190            (Some(discv5), Some(discv5_updates))
191        } else {
192            (None, None)
193        };
194
195        // In shared-port mode, spawn a task that peels `UnrecognizedFrame` events off the discv5
196        // update stream and feeds them into discv4's ingress. Other events are forwarded through
197        // a new channel that `Discovery::poll` reads. This keeps both protocols moving without
198        // requiring the main `Discovery::poll` loop to be driven for packets to be routed.
199        let (discv5_updates, _discv5_forwarder) = match (discv4_ingress, discv5_updates) {
200            (Some(mut ingress), Some(mut updates)) => {
201                let (tx, rx) = mpsc::channel(updates.max_capacity());
202                let handle = tokio::spawn(async move {
203                    while let Some(event) = updates.recv().await {
204                        if let discv5::Event::UnrecognizedFrame(frame) = &event {
205                            ingress.handle_packet(&frame.packet, frame.src_address).await;
206                            continue;
207                        }
208                        if tx.send(event).await.is_err() {
209                            break;
210                        }
211                    }
212                });
213                (Some(ReceiverStream::new(rx)), Some(handle))
214            }
215            (_, updates) => (updates.map(ReceiverStream::new), None),
216        };
217
218        // setup DNS discovery
219        let (_dns_discovery, dns_discovery_updates, _dns_disc_service) =
220            if let Some(dns_config) = dns_discovery_config {
221                let (mut service, dns_disc) = DnsDiscoveryService::new_pair(
222                    Arc::new(DnsResolver::from_system_conf()?),
223                    dns_config,
224                );
225                let dns_discovery_updates = service.node_record_stream();
226                let dns_disc_service = service.spawn();
227                (Some(dns_disc), Some(dns_discovery_updates), Some(dns_disc_service))
228            } else {
229                (None, None, None)
230            };
231
232        Ok(Self {
233            discovery_listeners: Default::default(),
234            local_enr,
235            discv4,
236            discv4_updates,
237            _discv4_service,
238            discv5,
239            discv5_updates,
240            _discv5_forwarder,
241            discovered_nodes: LruMap::new(DEFAULT_MAX_CAPACITY_DISCOVERED_PEERS_CACHE),
242            queued_events: Default::default(),
243            _dns_disc_service,
244            _dns_discovery,
245            dns_discovery_updates,
246        })
247    }
248
249    /// Registers a listener for receiving [`DiscoveryEvent`] updates.
250    pub(crate) fn add_listener(&mut self, tx: mpsc::UnboundedSender<DiscoveryEvent>) {
251        self.discovery_listeners.push(tx);
252    }
253
254    /// Notifies all registered listeners with the provided `event`.
255    #[inline]
256    fn notify_listeners(&mut self, event: &DiscoveryEvent) {
257        self.discovery_listeners.retain_mut(|listener| listener.send(event.clone()).is_ok());
258    }
259
260    /// Updates the `eth:ForkId` field in discv4/discv5.
261    pub(crate) fn update_fork_id(&self, fork_id: ForkId) {
262        if let Some(discv4) = &self.discv4 {
263            // use forward-compatible forkid entry
264            discv4.set_eip868_rlp(b"eth".to_vec(), EnrForkIdEntry::from(fork_id))
265        }
266        if let Some(discv5) = &self.discv5 {
267            discv5
268                .encode_and_set_eip868_in_local_enr(b"eth".to_vec(), EnrForkIdEntry::from(fork_id))
269        }
270    }
271
272    /// Bans the [`IpAddr`] in the discovery service.
273    pub(crate) fn ban_ip(&self, ip: IpAddr) {
274        if let Some(discv4) = &self.discv4 {
275            discv4.ban_ip(ip)
276        }
277        if let Some(discv5) = &self.discv5 {
278            discv5.ban_ip(ip)
279        }
280    }
281
282    /// Bans the [`PeerId`] and [`IpAddr`] in the discovery service.
283    pub(crate) fn ban(&self, peer_id: PeerId, ip: IpAddr) {
284        if let Some(discv4) = &self.discv4 {
285            discv4.ban(peer_id, ip)
286        }
287        if let Some(discv5) = &self.discv5 {
288            discv5.ban(peer_id, ip)
289        }
290    }
291
292    /// Returns a shared reference to the discv4.
293    pub fn discv4(&self) -> Option<Discv4> {
294        self.discv4.clone()
295    }
296
297    /// Returns the id with which the local node identifies itself in the network
298    pub(crate) const fn local_id(&self) -> PeerId {
299        self.local_enr.id // local discv4 and discv5 have same id, since signed with same secret key
300    }
301
302    /// Add a node to the discv4 table.
303    pub(crate) fn add_discv4_node(&self, node: NodeRecord) {
304        if let Some(discv4) = &self.discv4 {
305            discv4.add_node(node);
306        }
307    }
308
309    /// Returns discv5 handle.
310    pub fn discv5(&self) -> Option<Discv5> {
311        self.discv5.clone()
312    }
313
314    /// Add a node to the discv4 table.
315    pub(crate) fn add_discv5_node(&self, enr: Enr<SecretKey>) -> Result<(), NetworkError> {
316        if let Some(discv5) = &self.discv5 {
317            discv5.add_node(enr).map_err(NetworkError::Discv5Error)?;
318        }
319
320        Ok(())
321    }
322
323    /// Processes an incoming [`NodeRecord`] update from a discovery service
324    fn on_node_record_update(&mut self, record: NodeRecord, fork_id: Option<ForkId>) {
325        let peer_id = record.id;
326        let tcp_addr = record.tcp_addr();
327        if tcp_addr.port() == 0 {
328            // useless peer for p2p
329            return
330        }
331        let udp_addr = record.udp_addr();
332        let addr = PeerAddr::new(tcp_addr, Some(udp_addr));
333        _ =
334            self.discovered_nodes.get_or_insert(peer_id, || {
335                self.queued_events.push_back(DiscoveryEvent::NewNode(
336                    DiscoveredEvent::EventQueued { peer_id, addr, fork_id },
337                ));
338
339                addr
340            })
341    }
342
343    fn on_discv4_update(&mut self, update: DiscoveryUpdate) {
344        match update {
345            DiscoveryUpdate::Added(record) | DiscoveryUpdate::DiscoveredAtCapacity(record) => {
346                self.on_node_record_update(record, None);
347            }
348            DiscoveryUpdate::EnrForkId(node, fork_id) => {
349                self.queued_events.push_back(DiscoveryEvent::EnrForkId(node, fork_id))
350            }
351            DiscoveryUpdate::Removed(peer_id) => {
352                self.discovered_nodes.remove(&peer_id);
353            }
354            DiscoveryUpdate::Batch(updates) => {
355                for update in updates {
356                    self.on_discv4_update(update);
357                }
358            }
359        }
360    }
361
362    pub(crate) fn poll(&mut self, cx: &mut Context<'_>) -> Poll<DiscoveryEvent> {
363        loop {
364            // Drain all buffered events first
365            if let Some(event) = self.queued_events.pop_front() {
366                self.notify_listeners(&event);
367                return Poll::Ready(event)
368            }
369
370            // drain the discv4 update stream
371            while let Some(Poll::Ready(Some(update))) =
372                self.discv4_updates.as_mut().map(|updates| updates.poll_next_unpin(cx))
373            {
374                self.on_discv4_update(update)
375            }
376
377            // drain the discv5 update stream
378            while let Some(Poll::Ready(Some(update))) =
379                self.discv5_updates.as_mut().map(|updates| updates.poll_next_unpin(cx))
380            {
381                if let Some(discv5) = self.discv5.as_mut() &&
382                    let Some(DiscoveredPeer { node_record, fork_id }) =
383                        discv5.on_discv5_update(update)
384                {
385                    self.on_node_record_update(node_record, fork_id);
386                }
387            }
388
389            // drain the dns update stream
390            while let Some(Poll::Ready(Some(update))) =
391                self.dns_discovery_updates.as_mut().map(|updates| updates.poll_next_unpin(cx))
392            {
393                self.add_discv4_node(update.node_record);
394                if let Err(err) = self.add_discv5_node(update.enr) {
395                    trace!(target: "net::discovery",
396                        %err,
397                        "failed adding node discovered by dns to discv5"
398                    );
399                }
400                self.on_node_record_update(update.node_record, update.fork_id);
401            }
402
403            if self.queued_events.is_empty() {
404                return Poll::Pending
405            }
406        }
407    }
408}
409
410const fn set_bound_rlpx_port_if_unset(config: &mut reth_discv5::Config, port: u16) {
411    if config.rlpx_socket().port() == 0 {
412        config.set_rlpx_port(port);
413    }
414}
415
416impl Drop for Discovery {
417    fn drop(&mut self) {
418        if let Some(discv4) = &self.discv4 {
419            discv4.terminate();
420        }
421        if let Some(handle) = self._discv4_service.take() {
422            handle.abort();
423        }
424        if let Some(handle) = self._discv5_forwarder.take() {
425            handle.abort();
426        }
427        if let Some(handle) = self._dns_disc_service.take() {
428            handle.abort();
429        }
430    }
431}
432
433impl Stream for Discovery {
434    type Item = DiscoveryEvent;
435
436    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
437        Poll::Ready(Some(ready!(self.get_mut().poll(cx))))
438    }
439}
440
441#[cfg(test)]
442impl Discovery {
443    /// Returns a Discovery instance that does nothing and is intended for testing purposes.
444    ///
445    /// NOTE: This instance does nothing
446    pub(crate) fn noop() -> Self {
447        let (_discovery_listeners, _): (mpsc::UnboundedSender<DiscoveryEvent>, _) =
448            mpsc::unbounded_channel();
449
450        Self {
451            discovered_nodes: LruMap::new(0),
452            local_enr: NodeRecord {
453                address: IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
454                tcp_port: 0,
455                udp_port: 0,
456                id: PeerId::random(),
457            },
458            discv4: Default::default(),
459            discv4_updates: Default::default(),
460            _discv4_service: Default::default(),
461            _discv5_forwarder: None,
462            discv5: None,
463            discv5_updates: None,
464            queued_events: Default::default(),
465            _dns_discovery: None,
466            dns_discovery_updates: None,
467            _dns_disc_service: None,
468            discovery_listeners: Default::default(),
469        }
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use secp256k1::SECP256K1;
477    use std::net::{Ipv4Addr, SocketAddrV4};
478
479    #[tokio::test(flavor = "multi_thread")]
480    async fn test_discovery_setup() {
481        let (secret_key, _) = SECP256K1.generate_keypair(&mut rand_08::thread_rng());
482        let discovery_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0));
483        let _discovery = Discovery::new(
484            discovery_addr,
485            discovery_addr,
486            secret_key,
487            Default::default(),
488            None,
489            Default::default(),
490        )
491        .await
492        .unwrap();
493    }
494
495    use reth_discv4::Discv4ConfigBuilder;
496    use reth_discv5::{enr::EnrCombinedKeyWrapper, enr_to_discv4_id};
497    use tracing::trace;
498
499    async fn start_discovery_node(udp_port_discv4: u16, udp_port_discv5: u16) -> Discovery {
500        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
501
502        let discv4_addr = format!("127.0.0.1:{udp_port_discv4}").parse().unwrap();
503        let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
504
505        // disable `NatResolver`
506        let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
507
508        let discv5_listen_config = discv5::ListenConfig::from(discv5_addr);
509        let discv5_config = reth_discv5::Config::builder(discv5_addr)
510            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
511            .build();
512
513        Discovery::new(
514            discv4_addr,
515            discv4_addr,
516            secret_key,
517            Some(discv4_config),
518            Some(discv5_config),
519            None,
520        )
521        .await
522        .expect("should build discv5 with discv4 downgrade")
523    }
524
525    #[test]
526    fn discv5_enr_advertises_bound_rlpx_port() {
527        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
528
529        let bound_rlpx_port = 30307;
530        let discv5_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
531        let mut discv5_config = reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 0).into())
532            .discv5_config(discv5::ConfigBuilder::new(discv5_addr.into()).build())
533            .build();
534
535        set_bound_rlpx_port_if_unset(&mut discv5_config, bound_rlpx_port);
536
537        let (enr, _, _, _) = reth_discv5::build_local_enr(&secret_key, &discv5_config);
538        assert_eq!(enr.tcp4(), Some(bound_rlpx_port));
539    }
540
541    #[test]
542    fn discv5_enr_preserves_configured_rlpx_port() {
543        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
544
545        let advertised_addr: SocketAddr = "127.0.0.1:30308".parse().unwrap();
546        let discv5_addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
547        let mut discv5_config = reth_discv5::Config::builder(advertised_addr)
548            .discv5_config(discv5::ConfigBuilder::new(discv5_addr.into()).build())
549            .build();
550
551        set_bound_rlpx_port_if_unset(&mut discv5_config, 30307);
552
553        let (enr, _, _, _) = reth_discv5::build_local_enr(&secret_key, &discv5_config);
554        assert_eq!(enr.tcp4(), Some(advertised_addr.port()));
555    }
556
557    #[tokio::test(flavor = "multi_thread")]
558    async fn discv5_and_discv4_same_pk() {
559        reth_tracing::init_test_tracing();
560
561        // set up test
562        let mut node_1 = start_discovery_node(40014, 40015).await;
563        let discv4_enr_1 = node_1.discv4.as_ref().unwrap().node_record();
564        let discv5_enr_node_1 =
565            node_1.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
566        let discv4_id_1 = discv4_enr_1.id;
567        let discv5_id_1 = discv5_enr_node_1.node_id();
568
569        let mut node_2 = start_discovery_node(40024, 40025).await;
570        let discv4_enr_2 = node_2.discv4.as_ref().unwrap().node_record();
571        let discv5_enr_node_2 =
572            node_2.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
573        let discv4_id_2 = discv4_enr_2.id;
574        let discv5_id_2 = discv5_enr_node_2.node_id();
575
576        trace!(target: "net::discovery::tests",
577            node_1_node_id=format!("{:#}", discv5_id_1),
578            node_2_node_id=format!("{:#}", discv5_id_2),
579            "started nodes"
580        );
581
582        // test
583
584        // assert discovery version 4 and version 5 nodes have same id
585        assert_eq!(discv4_id_1, enr_to_discv4_id(&discv5_enr_node_1).unwrap());
586        assert_eq!(discv4_id_2, enr_to_discv4_id(&discv5_enr_node_2).unwrap());
587
588        // add node_2:discv4 manually to node_1:discv4
589        node_1.add_discv4_node(discv4_enr_2);
590
591        // verify node_2:discv4 discovered node_1:discv4 and vv
592        let event_node_1 = node_1.next().await.unwrap();
593        let event_node_2 = node_2.next().await.unwrap();
594
595        assert_eq!(
596            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
597                peer_id: discv4_id_2,
598                addr: PeerAddr::new(discv4_enr_2.tcp_addr(), Some(discv4_enr_2.udp_addr())),
599                fork_id: None
600            }),
601            event_node_1
602        );
603        assert_eq!(
604            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
605                peer_id: discv4_id_1,
606                addr: PeerAddr::new(discv4_enr_1.tcp_addr(), Some(discv4_enr_1.udp_addr())),
607                fork_id: None
608            }),
609            event_node_2
610        );
611
612        assert_eq!(1, node_1.discovered_nodes.len());
613        assert_eq!(1, node_2.discovered_nodes.len());
614
615        // add node_2:discv5 to node_1:discv5, manual insertion won't emit an event
616        node_1.add_discv5_node(EnrCombinedKeyWrapper(discv5_enr_node_2.clone()).into()).unwrap();
617        // verify node_2 is in KBuckets of node_1:discv5
618        assert!(node_1
619            .discv5
620            .as_ref()
621            .unwrap()
622            .with_discv5(|discv5| discv5.table_entries_id().contains(&discv5_id_2)));
623
624        // manually trigger connection from node_1:discv5 to node_2:discv5
625        node_1
626            .discv5
627            .as_ref()
628            .unwrap()
629            .with_discv5(|discv5| discv5.send_ping(discv5_enr_node_2.clone()))
630            .await
631            .unwrap();
632
633        // this won't emit an event, since the nodes already discovered each other on discv4, the
634        // number of nodes stored for each node on this level remains 1.
635        assert_eq!(1, node_1.discovered_nodes.len());
636        assert_eq!(1, node_2.discovered_nodes.len());
637    }
638
639    /// Starts a discovery node with discv4 and discv5 sharing the same UDP port.
640    async fn start_shared_port_node(port: u16) -> Discovery {
641        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
642        let disc_addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
643        // Use a non-zero TCP port so the node record isn't filtered out by
644        // `on_node_record_update` (which drops peers with tcp port == 0).
645        let tcp_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
646
647        let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
648
649        let discv5_listen_config = discv5::ListenConfig::from(disc_addr);
650        let discv5_config = reth_discv5::Config::builder(tcp_addr)
651            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
652            .build();
653
654        // Both protocols use the same address, triggering shared-port mode
655        Discovery::new(
656            tcp_addr,
657            disc_addr,
658            secret_key,
659            Some(discv4_config),
660            Some(discv5_config),
661            None,
662        )
663        .await
664        .expect("should start with shared port")
665    }
666
667    #[tokio::test(flavor = "multi_thread")]
668    async fn test_shared_port_setup() {
669        reth_tracing::init_test_tracing();
670
671        // Use port 0 so the OS picks a free port
672        let node = start_shared_port_node(0).await;
673
674        // Both protocols should be active
675        assert!(node.discv4.is_some(), "discv4 should be running");
676        assert!(node.discv5.is_some(), "discv5 should be running");
677    }
678
679    #[tokio::test(flavor = "multi_thread")]
680    async fn test_shared_port_discv5_discovery() {
681        reth_tracing::init_test_tracing();
682
683        let mut node_1 = start_shared_port_node(0).await;
684        let mut node_2 = start_shared_port_node(0).await;
685
686        let discv5_enr_1 = node_1.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
687        let discv5_enr_2 = node_2.discv5.as_ref().unwrap().with_discv5(|discv5| discv5.local_enr());
688
689        let peer_id_1 = enr_to_discv4_id(&discv5_enr_1).unwrap();
690        let peer_id_2 = enr_to_discv4_id(&discv5_enr_2).unwrap();
691
692        // Add node_2's ENR to node_1's discv5 kbuckets and trigger a ping to establish a session.
693        // send_ping awaits the PONG, so the handshake completes before we poll the Discovery
694        // stream. The discv5 service runs its own background task.
695        node_1.add_discv5_node(EnrCombinedKeyWrapper(discv5_enr_2.clone()).into()).unwrap();
696        node_1
697            .discv5
698            .as_ref()
699            .unwrap()
700            .with_discv5(|discv5| discv5.send_ping(discv5_enr_2))
701            .await
702            .unwrap();
703
704        // Both SessionEstablished events should now be buffered in the update channels.
705        // Drive both nodes concurrently to collect them.
706        let mut event_1 = None;
707        let mut event_2 = None;
708        let timeout = tokio::time::sleep(std::time::Duration::from_secs(5));
709        tokio::pin!(timeout);
710        loop {
711            tokio::select! {
712                ev = node_1.next(), if event_1.is_none() => {
713                    event_1 = ev;
714                }
715                ev = node_2.next(), if event_2.is_none() => {
716                    event_2 = ev;
717                }
718                _ = &mut timeout => {
719                    panic!("timed out waiting for discv5 discovery events");
720                }
721            }
722            if event_1.is_some() && event_2.is_some() {
723                break;
724            }
725        }
726
727        assert!(matches!(
728            event_1.unwrap(),
729            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued { peer_id, .. })
730                if peer_id == peer_id_2
731        ));
732        assert!(matches!(
733            event_2.unwrap(),
734            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued { peer_id, .. })
735                if peer_id == peer_id_1
736        ));
737    }
738
739    #[tokio::test(flavor = "multi_thread")]
740    async fn test_shared_port_discv4_discovery() {
741        reth_tracing::init_test_tracing();
742
743        let mut node_1 = start_shared_port_node(0).await;
744        let mut node_2 = start_shared_port_node(0).await;
745
746        let enr_1 = node_1.discv4.as_ref().unwrap().node_record();
747        let enr_2 = node_2.discv4.as_ref().unwrap().node_record();
748
749        // Introduce node_2 to node_1 via discv4
750        node_1.add_discv4_node(enr_2);
751
752        // Both nodes should discover each other via discv4 ping/pong
753        let event_1 = node_1.next().await.unwrap();
754        let event_2 = node_2.next().await.unwrap();
755
756        assert_eq!(
757            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
758                peer_id: enr_2.id,
759                addr: PeerAddr::new(enr_2.tcp_addr(), Some(enr_2.udp_addr())),
760                fork_id: None
761            }),
762            event_1
763        );
764        assert_eq!(
765            DiscoveryEvent::NewNode(DiscoveredEvent::EventQueued {
766                peer_id: enr_1.id,
767                addr: PeerAddr::new(enr_1.tcp_addr(), Some(enr_1.udp_addr())),
768                fork_id: None
769            }),
770            event_2
771        );
772    }
773
774    /// Verifies that shared-port mode binds correctly when discv5 is configured for dual-stack.
775    /// On Linux this exercises the IPv6 V6ONLY path: without it, the IPv4 sibling would clash
776    /// with the IPv6 socket bound to the same port.
777    #[tokio::test(flavor = "multi_thread")]
778    async fn test_shared_port_dual_stack() {
779        reth_tracing::init_test_tracing();
780
781        // Find a port that's free on the v4 wildcard so we can use it for both v4 and v6.
782        let probe = UdpSocket::bind("0.0.0.0:0").await.expect("probe bind");
783        let port = probe.local_addr().unwrap().port();
784        drop(probe);
785
786        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
787        let v4_addr: SocketAddr = format!("0.0.0.0:{port}").parse().unwrap();
788        let tcp_addr: SocketAddr = "0.0.0.0:30303".parse().unwrap();
789
790        let discv4_config = Discv4ConfigBuilder::default().external_ip_resolver(None).build();
791
792        let discv5_listen_config = discv5::ListenConfig::DualStack {
793            ipv4: std::net::Ipv4Addr::UNSPECIFIED,
794            ipv4_port: port,
795            ipv6: std::net::Ipv6Addr::UNSPECIFIED,
796            ipv6_port: port,
797        };
798        let discv5_config = reth_discv5::Config::builder(tcp_addr)
799            .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
800            .build();
801
802        Discovery::new(
803            tcp_addr,
804            v4_addr,
805            secret_key,
806            Some(discv4_config),
807            Some(discv5_config),
808            None,
809        )
810        .await
811        .expect("discovery should start with shared port + dual-stack");
812    }
813}