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