1#![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 alloy_primitives::bytes::Bytes;
20use enr::{discv4_id_to_discv5_id, EnrCombinedKeyWrapper};
21use futures::future::join_all;
22use itertools::Itertools;
23use rand::{Rng, RngCore};
24use reth_ethereum_forks::{EnrForkIdEntry, ForkId};
25use reth_network_peers::{NodeRecord, PeerId};
26use secp256k1::SecretKey;
27use tokio::{sync::mpsc, task};
28use tracing::{debug, error, trace};
29
30pub mod config;
31pub mod enr;
32pub mod error;
33pub mod filter;
34pub mod metrics;
35pub mod network_stack_id;
36
37pub use ::enr::{CombinedKey, Enr, EnrKey, NodeId};
38pub use discv5::{self, IpMode};
39pub use libp2p_identity;
40
41pub use config::{
42 BootNode, Config, ConfigBuilder, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_ADDR,
43 DEFAULT_DISCOVERY_V5_ADDR_IPV6, DEFAULT_DISCOVERY_V5_LISTEN_CONFIG, DEFAULT_DISCOVERY_V5_PORT,
44 DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL,
45};
46pub use enr::enr_to_discv4_id;
47pub use error::Error;
48pub use filter::{FilterOutcome, MustNotIncludeKeys};
49pub use network_stack_id::NetworkStackId;
50
51use metrics::{DiscoveredPeersMetrics, Discv5Metrics};
52
53pub const MAX_KBUCKET_INDEX: usize = 255;
57
58pub const DEFAULT_MIN_TARGET_KBUCKET_INDEX: usize = 0;
64
65#[derive(Clone)]
67pub struct Discv5 {
68 discv5: Arc<discv5::Discv5>,
70 rlpx_ip_mode: IpMode,
72 fork_key: Option<&'static [u8]>,
74 discovered_peer_filter: MustNotIncludeKeys,
76 metrics: Discv5Metrics,
78 local_node_record: NodeRecord,
83}
84
85impl Discv5 {
86 pub fn add_node(&self, node_record: Enr<SecretKey>) -> Result<(), Error> {
92 let EnrCombinedKeyWrapper(enr) = node_record.try_into().map_err(Error::InvalidEnr)?;
93 self.discv5.add_enr(enr).map_err(Error::AddNodeFailed)
94 }
95
96 pub fn set_eip868_in_local_enr(&self, key: Vec<u8>, rlp: Bytes) {
102 let Ok(key_str) = std::str::from_utf8(&key) else {
103 error!(target: "net::discv5",
104 err="key not utf-8",
105 "failed to update local enr"
106 );
107 return
108 };
109 if let Err(err) = self.discv5.enr_insert(key_str, &rlp) {
110 error!(target: "net::discv5",
111 %err,
112 "failed to update local enr"
113 );
114 }
115 }
116
117 pub fn encode_and_set_eip868_in_local_enr(
121 &self,
122 key: Vec<u8>,
123 value: impl alloy_rlp::Encodable,
124 ) {
125 let mut buf = Vec::new();
126 value.encode(&mut buf);
127 self.set_eip868_in_local_enr(key, buf.into())
128 }
129
130 pub fn ban(&self, peer_id: PeerId, ip: IpAddr) {
134 match discv4_id_to_discv5_id(peer_id) {
135 Ok(node_id) => {
136 self.discv5.ban_node(&node_id, None);
137 self.ban_ip(ip);
138 }
139 Err(err) => error!(target: "net::discv5",
140 %err,
141 "failed to ban peer"
142 ),
143 }
144 }
145
146 pub fn ban_ip(&self, ip: IpAddr) {
150 self.discv5.ban_ip(ip, None);
151 }
152
153 pub fn node_record(&self) -> Option<NodeRecord> {
159 let enr: Enr<_> = EnrCombinedKeyWrapper(self.discv5.local_enr()).into();
160 enr.try_into().ok()
161 }
162
163 pub fn local_enr(&self) -> Enr<CombinedKey> {
165 self.discv5.local_enr()
166 }
167
168 pub const fn local_port(&self) -> u16 {
170 self.local_node_record.udp_port
171 }
172
173 pub async fn start(
177 sk: &SecretKey,
178 discv5_config: Config,
179 ) -> Result<(Self, mpsc::Receiver<discv5::Event>), Error> {
180 let (enr, local_node_record, fork_key, rlpx_ip_mode) = build_local_enr(sk, &discv5_config);
184
185 trace!(target: "net::discv5", ?enr, "local ENR");
186
187 let Config {
191 discv5_config,
192 bootstrap_nodes,
193 lookup_interval,
194 bootstrap_lookup_interval,
195 bootstrap_lookup_countdown,
196 discovered_peer_filter,
197 ..
198 } = discv5_config;
199
200 let EnrCombinedKeyWrapper(enr) = enr.try_into().map_err(Error::InvalidEnr)?;
201 let sk = CombinedKey::secp256k1_from_bytes(&mut sk.to_secret_bytes()).unwrap();
202 let mut discv5 = match discv5::Discv5::new(enr, sk, discv5_config) {
203 Ok(discv5) => discv5,
204 Err(err) => return Err(Error::InitFailure(err)),
205 };
206 discv5.start().await.map_err(Error::Discv5Error)?;
207
208 let discv5_updates = discv5.event_stream().await.map_err(Error::Discv5Error)?;
210
211 let discv5 = Arc::new(discv5);
212
213 bootstrap(bootstrap_nodes, &discv5).await?;
217
218 let metrics = Discv5Metrics::default();
219
220 spawn_populate_kbuckets_bg(
224 lookup_interval,
225 bootstrap_lookup_interval,
226 bootstrap_lookup_countdown,
227 metrics.clone(),
228 Arc::downgrade(&discv5),
229 );
230
231 Ok((
232 Self {
233 discv5,
234 rlpx_ip_mode,
235 fork_key,
236 discovered_peer_filter,
237 metrics,
238 local_node_record,
239 },
240 discv5_updates,
241 ))
242 }
243
244 pub fn on_discv5_update(&self, update: discv5::Event) -> Option<DiscoveredPeer> {
246 #[expect(clippy::match_same_arms)]
247 match update {
248 discv5::Event::SocketUpdated(_) | discv5::Event::TalkRequest(_) |
249 discv5::Event::Discovered(_) |
251 discv5::Event::UnrecognizedFrame(_) => None,
253 discv5::Event::NodeInserted { .. } => {
254
255 self.metrics.discovered_peers.increment_kbucket_insertions(1);
260
261 None
262 }
263 discv5::Event::SessionEstablished(enr, remote_socket) => {
264 self.metrics.discovered_peers.increment_established_sessions_raw(1);
272
273 self.on_discovered_peer(&enr, remote_socket)
274 }
275 discv5::Event::UnverifiableEnr {
276 enr,
277 socket,
278 node_id: _,
279 } => {
280 trace!(target: "net::discv5",
295 ?enr,
296 %socket,
297 "discovered unverifiable enr, source socket doesn't match socket advertised in ENR"
298 );
299
300 self.metrics.discovered_peers.increment_unverifiable_enrs_raw_total(1);
301
302 self.on_discovered_peer(&enr, socket)
303 }
304 _ => None
305 }
306 }
307
308 pub fn on_discovered_peer(
310 &self,
311 enr: &discv5::Enr,
312 socket: SocketAddr,
313 ) -> Option<DiscoveredPeer> {
314 self.metrics.discovered_peers_advertised_networks.increment_once_by_network_type(enr);
315
316 let node_record = match self.try_into_reachable(enr, socket) {
317 Ok(enr_bc) => enr_bc,
318 Err(err) => {
319 trace!(target: "net::discv5",
320 %err,
321 ?enr,
322 "discovered peer is unreachable"
323 );
324
325 self.metrics.discovered_peers.increment_established_sessions_unreachable_enr(1);
326
327 return None
328 }
329 };
330 if let FilterOutcome::Ignore { reason } = self.filter_discovered_peer(enr) {
331 trace!(target: "net::discv5",
332 ?enr,
333 reason,
334 "filtered out discovered peer"
335 );
336
337 self.metrics.discovered_peers.increment_established_sessions_filtered(1);
338
339 return None
340 }
341
342 let fork_id = self.get_fork_id(enr).ok();
343
344 trace!(target: "net::discv5",
345 ?fork_id,
346 ?enr,
347 "discovered peer"
348 );
349
350 Some(DiscoveredPeer { node_record, fork_id })
351 }
352
353 pub fn try_into_reachable(
363 &self,
364 enr: &discv5::Enr,
365 socket: SocketAddr,
366 ) -> Result<NodeRecord, Error> {
367 let address = socket.ip();
369 let udp_port = socket.port();
370
371 let id = enr_to_discv4_id(enr).ok_or(Error::IncompatibleKeyType)?;
372
373 let tcp_port = (match address.to_canonical() {
379 IpAddr::V4(_) => enr.tcp4(),
380 IpAddr::V6(_) => enr.tcp6().or_else(|| enr.tcp4()),
381 })
382 .unwrap_or(
383 udp_port,
389 );
390
391 Ok(NodeRecord { address, tcp_port, udp_port, id })
392 }
393
394 pub fn filter_discovered_peer(&self, enr: &discv5::Enr) -> FilterOutcome {
397 self.discovered_peer_filter.filter(enr)
398 }
399
400 pub fn get_fork_id<K: EnrKey>(&self, enr: &Enr<K>) -> Result<ForkId, Error> {
403 let Some(key) = self.fork_key else { return Err(Error::NetworkStackIdNotConfigured) };
404 let fork_id = enr
405 .get_decodable::<EnrForkIdEntry>(key)
406 .or_else(|| {
407 (key != NetworkStackId::ETH)
408 .then(|| {
409 trace!(target: "net::discv5",
411 key = %String::from_utf8_lossy(key),
412 "Fork id not found for key, trying 'eth'..."
413 );
414 enr.get_decodable::<EnrForkIdEntry>(NetworkStackId::ETH)
415 })
416 .flatten()
417 })
418 .ok_or({
419 trace!(target: "net::discv5", "Fork id not found for 'eth' network stack id");
420 Error::ForkMissing(key)
421 })?
422 .map(Into::into)?;
423
424 Ok(fork_id)
425 }
426
427 pub fn with_discv5<F, R>(&self, f: F) -> R
433 where
434 F: FnOnce(&discv5::Discv5) -> R,
435 {
436 f(&self.discv5)
437 }
438
439 pub const fn ip_mode(&self) -> IpMode {
445 self.rlpx_ip_mode
446 }
447
448 pub const fn fork_key(&self) -> Option<&[u8]> {
450 self.fork_key
451 }
452}
453
454impl fmt::Debug for Discv5 {
455 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456 "{ .. }".fmt(f)
457 }
458}
459
460#[derive(Debug)]
462pub struct DiscoveredPeer {
463 pub node_record: NodeRecord,
465 pub fork_id: Option<ForkId>,
467}
468
469pub fn build_local_enr(
471 sk: &SecretKey,
472 config: &Config,
473) -> (Enr<SecretKey>, NodeRecord, Option<&'static [u8]>, IpMode) {
474 let mut builder = Enr::builder();
475
476 let Config {
477 discv5_config,
478 fork,
479 tcp_socket,
480 advertised_ipv4,
481 advertised_ipv6,
482 other_enr_kv_pairs,
483 ..
484 } = config;
485
486 let socket = {
487 let v4 = crate::config::ipv4(&discv5_config.listen_config);
488 let v6 = crate::config::ipv6(&discv5_config.listen_config);
489
490 if let Some(addr) = v4 {
493 if let Some(ip) = advertised_ipv4 {
494 builder.ip4(*ip);
495 } else if *addr.ip() != Ipv4Addr::UNSPECIFIED {
496 builder.ip4(*addr.ip());
497 }
498 builder.udp4(addr.port());
499 }
500 if let Some(addr) = v6 {
501 if let Some(ip) = advertised_ipv6 {
502 builder.ip6(*ip);
503 } else if *addr.ip() != Ipv6Addr::UNSPECIFIED {
504 builder.ip6(*addr.ip());
505 }
506 builder.udp6(addr.port());
507 }
508 if v4.is_some() {
510 builder.tcp4(tcp_socket.port());
511 } else if v6.is_some() {
512 builder.tcp6(tcp_socket.port());
513 }
514
515 v6.map(SocketAddr::V6)
517 .or_else(|| v4.map(SocketAddr::V4))
518 .unwrap_or_else(|| SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)))
519 };
520
521 let rlpx_ip_mode = if tcp_socket.is_ipv4() { IpMode::Ip4 } else { IpMode::Ip6 };
522
523 let network_stack_id = fork.as_ref().map(|(network_stack_id, fork_value)| {
525 builder.add_value_rlp(network_stack_id, alloy_rlp::encode(fork_value).into());
526 *network_stack_id
527 });
528
529 for (key, value) in other_enr_kv_pairs {
531 builder.add_value_rlp(key, value.clone().into());
532 }
533
534 let enr = builder.build(sk).expect("should build enr v4");
537
538 let bc_enr = NodeRecord::from_secret_key(socket, sk);
540
541 (enr, bc_enr, network_stack_id, rlpx_ip_mode)
542}
543
544pub async fn bootstrap(
553 bootstrap_nodes: HashSet<BootNode>,
554 discv5: &Arc<discv5::Discv5>,
555) -> Result<(), Error> {
556 trace!(target: "net::discv5",
557 ?bootstrap_nodes,
558 "adding bootstrap nodes .."
559 );
560
561 let mut enr_requests = vec![];
562 for node in bootstrap_nodes {
563 match node {
564 BootNode::Enr(node) => {
565 if let Err(err) = discv5.add_enr(node) {
566 return Err(Error::AddNodeFailed(err))
567 }
568 }
569 BootNode::Enode(enode) => {
570 let discv5 = discv5.clone();
571 enr_requests.push(async move {
572 if let Err(err) = discv5.request_enr(enode.to_string()).await {
573 debug!(target: "net::discv5",
574 ?enode,
575 %err,
576 "failed adding boot node"
577 );
578 }
579 })
580 }
581 }
582 }
583
584 if !enr_requests.is_empty() {
586 task::spawn(join_all(enr_requests));
587 }
588
589 Ok(())
590}
591
592pub fn spawn_populate_kbuckets_bg(
594 lookup_interval: u64,
595 bootstrap_lookup_interval: u64,
596 bootstrap_lookup_countdown: u64,
597 metrics: Discv5Metrics,
598 discv5: std::sync::Weak<discv5::Discv5>,
599) {
600 let lookup_interval = Duration::from_secs(lookup_interval);
601 let metrics = metrics.discovered_peers;
602 let mut kbucket_index = MAX_KBUCKET_INDEX;
603 let pulse_lookup_interval = Duration::from_secs(bootstrap_lookup_interval);
604 task::spawn(async move {
605 let Some(discv5_handle) = discv5.upgrade() else {
606 return;
607 };
608 let local_node_id = discv5_handle.local_enr().node_id();
609 drop(discv5_handle);
610
611 for i in (0..bootstrap_lookup_countdown).rev() {
614 let target = NodeId::random();
615
616 trace!(target: "net::discv5",
617 %target,
618 bootstrap_boost_runs_countdown=i,
619 lookup_interval=format!("{:#?}", pulse_lookup_interval),
620 "starting bootstrap boost lookup query"
621 );
622
623 {
624 let Some(discv5_handle) = discv5.upgrade() else {
625 return;
626 };
627 lookup(target, &discv5_handle, &metrics).await;
628 }
629
630 tokio::time::sleep(pulse_lookup_interval).await;
631 }
632
633 loop {
635 let target = get_lookup_target(kbucket_index, local_node_id);
638
639 trace!(target: "net::discv5",
640 %target,
641 lookup_interval=format!("{:#?}", lookup_interval),
642 "starting periodic lookup query"
643 );
644
645 {
646 let Some(discv5_handle) = discv5.upgrade() else {
647 return;
648 };
649 lookup(target, &discv5_handle, &metrics).await;
650 }
651
652 if kbucket_index > DEFAULT_MIN_TARGET_KBUCKET_INDEX {
653 kbucket_index -= 1
655 } else {
656 kbucket_index = MAX_KBUCKET_INDEX
658 }
659
660 tokio::time::sleep(lookup_interval).await;
661 }
662 });
663}
664
665pub fn get_lookup_target(kbucket_index: usize, local_node_id: NodeId) -> NodeId {
667 let mut target = local_node_id.raw();
669
670 let bit_offset = MAX_KBUCKET_INDEX.saturating_sub(kbucket_index);
672 let (byte, bit) = (bit_offset / 8, bit_offset % 8);
673 target[byte] ^= 1 << (7 - bit);
675
676 let mut rng = rand::rng();
678 if bit < 7 {
680 let bits_to_randomize = 0xff >> (bit + 1);
682 target[byte] &= !bits_to_randomize;
684 target[byte] |= rng.random::<u8>() & bits_to_randomize;
686 }
687 rng.fill_bytes(&mut target[byte + 1..]);
689
690 target.into()
691}
692
693pub async fn lookup(target: NodeId, discv5: &discv5::Discv5, metrics: &DiscoveredPeersMetrics) {
695 metrics.set_total_sessions(discv5::Discv5::metrics().active_sessions);
696 metrics.set_total_kbucket_peers(
697 discv5.with_kbuckets(|kbuckets| kbuckets.read().iter_ref().count()),
698 );
699
700 match discv5.find_node(target).await {
701 Err(err) => trace!(target: "net::discv5",
702 %err,
703 "lookup query failed"
704 ),
705 Ok(peers) => trace!(target: "net::discv5",
706 target=format!("{:#?}", target),
707 peers_count=peers.len(),
708 peers=format!("[{:#}]", peers.iter()
709 .map(|enr| enr.node_id()
710 ).format(", ")),
711 "peers returned by lookup query"
712 ),
713 }
714
715 debug!(target: "net::discv5",
719 connected_peers=discv5.connected_peers(),
720 "connected peers in routing table"
721 );
722}
723
724#[cfg(test)]
725mod test {
726 #![allow(deprecated)]
727 use super::*;
728 use ::enr::{CombinedKey, EnrKey};
729 use discv5::ListenConfig;
730 use rand::rng;
731 use reth_chainspec::MAINNET;
732 use std::{
733 net::UdpSocket,
734 time::{Duration, Instant},
735 };
736 use tracing::trace;
737
738 fn discv5_noop() -> Discv5 {
739 let sk = CombinedKey::generate_secp256k1();
740 Discv5 {
741 discv5: Arc::new(
742 discv5::Discv5::new(
743 Enr::empty(&sk).unwrap(),
744 sk,
745 discv5::ConfigBuilder::new(DEFAULT_DISCOVERY_V5_LISTEN_CONFIG).build(),
746 )
747 .unwrap(),
748 ),
749 rlpx_ip_mode: IpMode::Ip4,
750 fork_key: None,
751 discovered_peer_filter: MustNotIncludeKeys::default(),
752 metrics: Discv5Metrics::default(),
753 local_node_record: NodeRecord::new(
754 (Ipv4Addr::LOCALHOST, 30303).into(),
755 PeerId::random(),
756 ),
757 }
758 }
759
760 async fn start_discovery_node(udp_port_discv5: u16) -> (Discv5, mpsc::Receiver<discv5::Event>) {
761 let secret_key = SecretKey::new(&mut rng());
762
763 let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
764 let rlpx_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
765
766 let discv5_listen_config = ListenConfig::from(discv5_addr);
767 let discv5_config = Config::builder(rlpx_addr)
768 .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
769 .build();
770
771 Discv5::start(&secret_key, discv5_config).await.expect("should build discv5")
772 }
773
774 async fn start_discovery_node_with_key(
775 secret_key: &SecretKey,
776 udp_port_discv5: u16,
777 ) -> Result<(Discv5, mpsc::Receiver<discv5::Event>), Error> {
778 let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
779 let rlpx_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
780
781 let discv5_listen_config = ListenConfig::from(discv5_addr);
782 let discv5_config = Config::builder(rlpx_addr)
783 .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
784 .build();
785
786 Discv5::start(secret_key, discv5_config).await
787 }
788
789 fn unused_udp_port() -> u16 {
790 UdpSocket::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
791 }
792
793 async fn wait_for_udp_port_release(port: u16, timeout: Duration) {
794 let deadline = Instant::now() + timeout;
795
796 loop {
797 match UdpSocket::bind(("127.0.0.1", port)) {
798 Ok(socket) => {
799 drop(socket);
800 return;
801 }
802 Err(err) if Instant::now() < deadline => {
803 trace!(target: "net::discv5::test", %port, %err, "waiting for discv5 port release");
804 tokio::time::sleep(Duration::from_millis(10)).await;
805 }
806 Err(err) => panic!("discv5 did not release port {port} before timeout: {err}"),
807 }
808 }
809 }
810
811 #[tokio::test(flavor = "multi_thread")]
812 async fn discv5_releases_port_on_drop() {
813 reth_tracing::init_test_tracing();
814
815 let secret_key = SecretKey::new(&mut rng());
816 let port = unused_udp_port();
817
818 let (node, updates) =
819 start_discovery_node_with_key(&secret_key, port).await.expect("should start discv5");
820 drop(updates);
821 drop(node);
822
823 wait_for_udp_port_release(port, Duration::from_secs(1)).await;
824
825 let restarted = start_discovery_node_with_key(&secret_key, port).await;
826 assert!(restarted.is_ok(), "discv5 failed to rebind dropped port: {restarted:?}");
827 }
828
829 #[tokio::test(flavor = "multi_thread")]
830 async fn discv5() {
831 reth_tracing::init_test_tracing();
832
833 let (node_1, mut stream_1) = start_discovery_node(30344).await;
837 let node_1_enr = node_1.with_discv5(|discv5| discv5.local_enr());
838
839 let (node_2, mut stream_2) = start_discovery_node(30355).await;
841 let node_2_enr = node_2.with_discv5(|discv5| discv5.local_enr());
842
843 trace!(target: "net::discv5::test",
844 node_1_node_id=format!("{:#}", node_1_enr.node_id()),
845 node_2_node_id=format!("{:#}", node_2_enr.node_id()),
846 "started nodes"
847 );
848
849 let node_2_enr_reth_compatible_ty: Enr<SecretKey> =
853 EnrCombinedKeyWrapper(node_2_enr.clone()).into();
854 node_1.add_node(node_2_enr_reth_compatible_ty).unwrap();
855
856 assert!(
858 node_1.with_discv5(|discv5| discv5.table_entries_id().contains(&node_2_enr.node_id()))
859 );
860
861 node_1.with_discv5(|discv5| discv5.send_ping(node_2_enr.clone())).await.unwrap();
863
864 let event_1_v5 = stream_1.recv().await.unwrap();
866
867 assert!(matches!(
868 event_1_v5,
869 discv5::Event::SessionEstablished(node, socket) if node == node_2_enr && socket == node_2_enr.udp4_socket().unwrap().into()
870 ));
871
872 let event_2_v5 = stream_2.recv().await.unwrap();
874 assert!(matches!(
875 event_2_v5,
876 discv5::Event::NodeInserted { node_id, replaced } if node_id == node_1_enr.node_id() && replaced.is_none()
877 ));
878 }
879
880 #[test]
881 fn discovered_enr_disc_socket_missing() {
882 reth_tracing::init_test_tracing();
883
884 const REMOTE_RLPX_PORT: u16 = 30303;
886 let remote_socket = "104.28.44.25:9000".parse().unwrap();
887 let remote_key = CombinedKey::generate_secp256k1();
888 let remote_enr = Enr::builder().tcp4(REMOTE_RLPX_PORT).build(&remote_key).unwrap();
889
890 let discv5 = discv5_noop();
891
892 let filtered_peer = discv5.on_discovered_peer(&remote_enr, remote_socket);
894
895 assert_eq!(
896 NodeRecord {
897 address: remote_socket.ip(),
898 udp_port: remote_socket.port(),
899 tcp_port: REMOTE_RLPX_PORT,
900 id: enr_to_discv4_id(&remote_enr).unwrap(),
901 },
902 filtered_peer.unwrap().node_record
903 )
904 }
905
906 #[test]
907 fn reachable_tcp_port_follows_session_address_family() {
908 const TCP4_PORT: u16 = 30304;
913 const TCP6_PORT: u16 = 30306;
914 let remote_key = CombinedKey::generate_secp256k1();
915 let remote_enr = Enr::builder().tcp4(TCP4_PORT).tcp6(TCP6_PORT).build(&remote_key).unwrap();
916
917 let v4_socket: SocketAddr = "127.0.0.1:30305".parse().unwrap();
918 let v6_socket: SocketAddr = "[::1]:30307".parse().unwrap();
919
920 for local_mode in [IpMode::Ip4, IpMode::Ip6] {
921 let mut discv5 = discv5_noop();
922 discv5.rlpx_ip_mode = local_mode;
923
924 let record = discv5.try_into_reachable(&remote_enr, v4_socket).unwrap();
925 assert_eq!(
926 record.tcp_port, TCP4_PORT,
927 "v4 session with local mode {local_mode:?} must dial the tcp4 port"
928 );
929 assert_eq!(record.address, v4_socket.ip());
930 assert_eq!(record.udp_port, v4_socket.port());
931
932 let record = discv5.try_into_reachable(&remote_enr, v6_socket).unwrap();
933 assert_eq!(
934 record.tcp_port, TCP6_PORT,
935 "v6 session with local mode {local_mode:?} must dial the tcp6 port"
936 );
937 assert_eq!(record.address, v6_socket.ip());
938 assert_eq!(record.udp_port, v6_socket.port());
939 }
940 }
941
942 #[test]
943 fn reachable_ipv4_mapped_session_uses_tcp4() {
944 const TCP4_PORT: u16 = 30304;
945 const TCP6_PORT: u16 = 30306;
946 let remote_key = CombinedKey::generate_secp256k1();
947 let remote_enr = Enr::builder().tcp4(TCP4_PORT).tcp6(TCP6_PORT).build(&remote_key).unwrap();
948 let mapped_socket: SocketAddr = "[::ffff:104.28.44.25]:9000".parse().unwrap();
949
950 for local_mode in [IpMode::Ip4, IpMode::Ip6] {
951 let mut discv5 = discv5_noop();
952 discv5.rlpx_ip_mode = local_mode;
953
954 let record = discv5.try_into_reachable(&remote_enr, mapped_socket).unwrap();
955 assert_eq!(record.tcp_port, TCP4_PORT);
956 assert_eq!(record.address, mapped_socket.ip());
957 assert_eq!(record.udp_port, mapped_socket.port());
958
959 let peer = discv5.on_discovered_peer(&remote_enr, mapped_socket).unwrap();
960 assert_eq!(peer.node_record, record);
961 }
962 }
963
964 #[test]
965 fn reachable_tcp_port_v6_session_falls_back_to_tcp4() {
966 const TCP4_PORT: u16 = 30304;
969 let remote_key = CombinedKey::generate_secp256k1();
970 let remote_enr = Enr::builder().tcp4(TCP4_PORT).build(&remote_key).unwrap();
971
972 let v6_socket: SocketAddr = "[::1]:30307".parse().unwrap();
973
974 let mut discv5 = discv5_noop();
975 discv5.rlpx_ip_mode = IpMode::Ip6;
976 let record = discv5.try_into_reachable(&remote_enr, v6_socket).unwrap();
977 assert_eq!(record.tcp_port, TCP4_PORT);
978 }
979
980 #[test]
981 fn reachable_tcp_port_missing_falls_back_to_udp_port() {
982 let remote_key = CombinedKey::generate_secp256k1();
985 let remote_enr = Enr::builder().build(&remote_key).unwrap();
986
987 let discv5 = discv5_noop();
988 let v4_socket: SocketAddr = "127.0.0.1:30305".parse().unwrap();
989 assert_eq!(discv5.try_into_reachable(&remote_enr, v4_socket).unwrap().tcp_port, 30305);
990 let v6_socket: SocketAddr = "[::1]:30307".parse().unwrap();
991 assert_eq!(discv5.try_into_reachable(&remote_enr, v6_socket).unwrap().tcp_port, 30307);
992 }
993
994 #[expect(unreachable_pub)]
997 #[expect(unused)]
998 mod sigp {
999 use alloy_primitives::U256;
1000 use enr::NodeId;
1001
1002 #[derive(Clone, Debug)]
1012 pub struct Key<T> {
1013 preimage: T,
1014 hash: [u8; 32],
1015 }
1016
1017 impl<T> PartialEq for Key<T> {
1018 fn eq(&self, other: &Self) -> bool {
1019 self.hash == other.hash
1020 }
1021 }
1022
1023 impl<T> Eq for Key<T> {}
1024
1025 impl<TPeerId> AsRef<Self> for Key<TPeerId> {
1026 fn as_ref(&self) -> &Self {
1027 self
1028 }
1029 }
1030
1031 impl<T> Key<T> {
1032 pub const fn new_raw(preimage: T, hash: [u8; 32]) -> Self {
1034 Self { preimage, hash }
1035 }
1036
1037 pub const fn preimage(&self) -> &T {
1039 &self.preimage
1040 }
1041
1042 pub fn into_preimage(self) -> T {
1044 self.preimage
1045 }
1046
1047 pub fn distance<U>(&self, other: &Key<U>) -> Distance {
1049 let a = U256::from_be_slice(self.hash.as_slice());
1050 let b = U256::from_be_slice(other.hash.as_slice());
1051 Distance(a ^ b)
1052 }
1053
1054 pub fn log2_distance<U>(&self, other: &Key<U>) -> Option<u64> {
1058 let xor_dist = self.distance(other);
1059 let log_dist = (256 - xor_dist.0.leading_zeros() as u64);
1060 (log_dist != 0).then_some(log_dist)
1061 }
1062 }
1063
1064 impl From<NodeId> for Key<NodeId> {
1065 fn from(node_id: NodeId) -> Self {
1066 Self { preimage: node_id, hash: node_id.raw() }
1067 }
1068 }
1069
1070 #[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Debug)]
1072 pub struct Distance(pub(super) U256);
1073 }
1074
1075 #[test]
1076 fn select_lookup_target() {
1077 for bucket_index in 0..=MAX_KBUCKET_INDEX {
1078 let sk = CombinedKey::generate_secp256k1();
1079 let local_node_id = NodeId::from(sk.public());
1080 let target = get_lookup_target(bucket_index, local_node_id);
1081
1082 let local_node_id = sigp::Key::from(local_node_id);
1083 let target = sigp::Key::from(target);
1084
1085 assert_eq!(local_node_id.log2_distance(&target), Some(bucket_index as u64 + 1));
1086 }
1087 }
1088
1089 #[test]
1090 fn build_enr_from_config() {
1091 const TCP_PORT: u16 = 30303;
1092 let fork_id = MAINNET.latest_fork_id();
1093
1094 let config = Config::builder((Ipv4Addr::UNSPECIFIED, TCP_PORT).into())
1095 .fork(NetworkStackId::ETH, fork_id)
1096 .build();
1097
1098 let sk = SecretKey::new(&mut rng());
1099 let (enr, _, _, _) = build_local_enr(&sk, &config);
1100
1101 let decoded_fork_id = enr
1102 .get_decodable::<EnrForkIdEntry>(NetworkStackId::ETH)
1103 .unwrap()
1104 .map(Into::into)
1105 .unwrap();
1106
1107 assert_eq!(fork_id, decoded_fork_id);
1108 assert_eq!(TCP_PORT, enr.tcp4().unwrap()); }
1110
1111 #[test]
1112 fn get_fork_id_with_different_network_stack_ids() {
1113 let fork_id = MAINNET.latest_fork_id();
1114 let sk = SecretKey::new(&mut rng());
1115
1116 let enr_with_opel = Enr::builder()
1118 .add_value_rlp(
1119 NetworkStackId::OPEL,
1120 alloy_rlp::encode(EnrForkIdEntry::from(fork_id)).into(),
1121 )
1122 .build(&sk)
1123 .unwrap();
1124
1125 let mut discv5 = discv5_noop();
1126 discv5.fork_key = Some(NetworkStackId::OPEL);
1127 assert_eq!(discv5.get_fork_id(&enr_with_opel).unwrap(), fork_id);
1128
1129 let enr_with_eth = Enr::builder()
1131 .add_value_rlp(
1132 NetworkStackId::ETH,
1133 alloy_rlp::encode(EnrForkIdEntry::from(fork_id)).into(),
1134 )
1135 .build(&sk)
1136 .unwrap();
1137
1138 discv5.fork_key = Some(NetworkStackId::OPEL);
1139 assert_eq!(discv5.get_fork_id(&enr_with_eth).unwrap(), fork_id);
1140
1141 let enr_without_network_stack_id = Enr::empty(&sk).unwrap();
1143 discv5.fork_key = Some(NetworkStackId::OPEL);
1144 assert!(matches!(
1145 discv5.get_fork_id(&enr_without_network_stack_id),
1146 Err(Error::ForkMissing(NetworkStackId::OPEL))
1147 ));
1148
1149 let discv5 = discv5_noop();
1151 assert!(matches!(
1152 discv5.get_fork_id(&enr_without_network_stack_id),
1153 Err(Error::NetworkStackIdNotConfigured)
1154 ));
1155 }
1156}