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 discv5::ListenConfig;
22use enr::{discv4_id_to_discv5_id, EnrCombinedKeyWrapper};
23use futures::future::join_all;
24use itertools::Itertools;
25use rand::{Rng, RngCore};
26use reth_ethereum_forks::{EnrForkIdEntry, ForkId};
27use reth_network_peers::{NodeRecord, PeerId};
28use secp256k1::SecretKey;
29use tokio::{sync::mpsc, task};
30use tracing::{debug, error, trace};
31
32pub mod config;
33pub mod enr;
34pub mod error;
35pub mod filter;
36pub mod metrics;
37pub mod network_stack_id;
38
39pub use discv5::{self, IpMode};
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.into();
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<discv5::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.into();
201 let sk = discv5::enr::CombinedKey::secp256k1_from_bytes(&mut sk.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 discv5.clone(),
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(_) => None,
251 discv5::Event::NodeInserted { replaced: _, .. } => {
252
253 self.metrics.discovered_peers.increment_kbucket_insertions(1);
258
259 None
260 }
261 discv5::Event::SessionEstablished(enr, remote_socket) => {
262 self.metrics.discovered_peers.increment_established_sessions_raw(1);
270
271 self.on_discovered_peer(&enr, remote_socket)
272 }
273 discv5::Event::UnverifiableEnr {
274 enr,
275 socket,
276 node_id: _,
277 } => {
278 trace!(target: "net::discv5",
293 ?enr,
294 %socket,
295 "discovered unverifiable enr, source socket doesn't match socket advertised in ENR"
296 );
297
298 self.metrics.discovered_peers.increment_unverifiable_enrs_raw_total(1);
299
300 self.on_discovered_peer(&enr, socket)
301 }
302 _ => None
303 }
304 }
305
306 pub fn on_discovered_peer(
308 &self,
309 enr: &discv5::Enr,
310 socket: SocketAddr,
311 ) -> Option<DiscoveredPeer> {
312 self.metrics.discovered_peers_advertised_networks.increment_once_by_network_type(enr);
313
314 let node_record = match self.try_into_reachable(enr, socket) {
315 Ok(enr_bc) => enr_bc,
316 Err(err) => {
317 trace!(target: "net::discv5",
318 %err,
319 ?enr,
320 "discovered peer is unreachable"
321 );
322
323 self.metrics.discovered_peers.increment_established_sessions_unreachable_enr(1);
324
325 return None
326 }
327 };
328 if let FilterOutcome::Ignore { reason } = self.filter_discovered_peer(enr) {
329 trace!(target: "net::discv5",
330 ?enr,
331 reason,
332 "filtered out discovered peer"
333 );
334
335 self.metrics.discovered_peers.increment_established_sessions_filtered(1);
336
337 return None
338 }
339
340 let fork_id = self.get_fork_id(enr).ok();
341
342 trace!(target: "net::discv5",
343 ?fork_id,
344 ?enr,
345 "discovered peer"
346 );
347
348 Some(DiscoveredPeer { node_record, fork_id })
349 }
350
351 pub fn try_into_reachable(
361 &self,
362 enr: &discv5::Enr,
363 socket: SocketAddr,
364 ) -> Result<NodeRecord, Error> {
365 let address = socket.ip();
367 let udp_port = socket.port();
368
369 let id = enr_to_discv4_id(enr).ok_or(Error::IncompatibleKeyType)?;
370
371 let tcp_port = (match self.rlpx_ip_mode {
372 IpMode::Ip4 => enr.tcp4(),
373 IpMode::Ip6 => enr.tcp6(),
374 IpMode::DualStack => unimplemented!("dual-stack support not implemented for rlpx"),
375 })
376 .unwrap_or(
377 udp_port,
383 );
384
385 Ok(NodeRecord { address, tcp_port, udp_port, id })
386 }
387
388 pub fn filter_discovered_peer(&self, enr: &discv5::Enr) -> FilterOutcome {
391 self.discovered_peer_filter.filter(enr)
392 }
393
394 pub fn get_fork_id<K: discv5::enr::EnrKey>(
397 &self,
398 enr: &discv5::enr::Enr<K>,
399 ) -> Result<ForkId, Error> {
400 let Some(key) = self.fork_key else { return Err(Error::NetworkStackIdNotConfigured) };
401 let fork_id = enr
402 .get_decodable::<EnrForkIdEntry>(key)
403 .or_else(|| {
404 (key != NetworkStackId::ETH)
405 .then(|| {
406 trace!(target: "net::discv5",
408 key = %String::from_utf8_lossy(key),
409 "Fork id not found for key, trying 'eth'..."
410 );
411 enr.get_decodable::<EnrForkIdEntry>(NetworkStackId::ETH)
412 })
413 .flatten()
414 })
415 .ok_or({
416 trace!(target: "net::discv5", "Fork id not found for 'eth' network stack id");
417 Error::ForkMissing(key)
418 })?
419 .map(Into::into)?;
420
421 Ok(fork_id)
422 }
423
424 pub fn with_discv5<F, R>(&self, f: F) -> R
430 where
431 F: FnOnce(&discv5::Discv5) -> R,
432 {
433 f(&self.discv5)
434 }
435
436 pub const fn ip_mode(&self) -> IpMode {
442 self.rlpx_ip_mode
443 }
444
445 pub const fn fork_key(&self) -> Option<&[u8]> {
447 self.fork_key
448 }
449}
450
451impl fmt::Debug for Discv5 {
452 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453 "{ .. }".fmt(f)
454 }
455}
456
457#[derive(Debug)]
459pub struct DiscoveredPeer {
460 pub node_record: NodeRecord,
462 pub fork_id: Option<ForkId>,
464}
465
466pub fn build_local_enr(
468 sk: &SecretKey,
469 config: &Config,
470) -> (Enr<SecretKey>, NodeRecord, Option<&'static [u8]>, IpMode) {
471 let mut builder = discv5::enr::Enr::builder();
472
473 let Config { discv5_config, fork, tcp_socket, other_enr_kv_pairs, .. } = config;
474
475 let socket = match discv5_config.listen_config {
476 ListenConfig::Ipv4 { ip, port } => {
477 if ip != Ipv4Addr::UNSPECIFIED {
478 builder.ip4(ip);
479 }
480 builder.udp4(port);
481 builder.tcp4(tcp_socket.port());
482
483 (ip, port).into()
484 }
485 ListenConfig::Ipv6 { ip, port } => {
486 if ip != Ipv6Addr::UNSPECIFIED {
487 builder.ip6(ip);
488 }
489 builder.udp6(port);
490 builder.tcp6(tcp_socket.port());
491
492 (ip, port).into()
493 }
494 ListenConfig::DualStack { ipv4, ipv4_port, ipv6, ipv6_port } => {
495 if ipv4 != Ipv4Addr::UNSPECIFIED {
496 builder.ip4(ipv4);
497 }
498 builder.udp4(ipv4_port);
499 builder.tcp4(tcp_socket.port());
500
501 if ipv6 != Ipv6Addr::UNSPECIFIED {
502 builder.ip6(ipv6);
503 }
504 builder.udp6(ipv6_port);
505
506 (ipv6, ipv6_port).into()
507 }
508 };
509
510 let rlpx_ip_mode = if tcp_socket.is_ipv4() { IpMode::Ip4 } else { IpMode::Ip6 };
511
512 let network_stack_id = fork.as_ref().map(|(network_stack_id, fork_value)| {
514 builder.add_value_rlp(network_stack_id, alloy_rlp::encode(fork_value).into());
515 *network_stack_id
516 });
517
518 for (key, value) in other_enr_kv_pairs {
520 builder.add_value_rlp(key, value.clone().into());
521 }
522
523 let enr = builder.build(sk).expect("should build enr v4");
526
527 let bc_enr = NodeRecord::from_secret_key(socket, sk);
529
530 (enr, bc_enr, network_stack_id, rlpx_ip_mode)
531}
532
533pub async fn bootstrap(
535 bootstrap_nodes: HashSet<BootNode>,
536 discv5: &Arc<discv5::Discv5>,
537) -> Result<(), Error> {
538 trace!(target: "net::discv5",
539 ?bootstrap_nodes,
540 "adding bootstrap nodes .."
541 );
542
543 let mut enr_requests = vec![];
544 for node in bootstrap_nodes {
545 match node {
546 BootNode::Enr(node) => {
547 if let Err(err) = discv5.add_enr(node) {
548 return Err(Error::AddNodeFailed(err))
549 }
550 }
551 BootNode::Enode(enode) => {
552 let discv5 = discv5.clone();
553 enr_requests.push(async move {
554 if let Err(err) = discv5.request_enr(enode.to_string()).await {
555 debug!(target: "net::discv5",
556 ?enode,
557 %err,
558 "failed adding boot node"
559 );
560 }
561 })
562 }
563 }
564 }
565
566 Ok(_ = join_all(enr_requests).await)
568}
569
570pub fn spawn_populate_kbuckets_bg(
572 lookup_interval: u64,
573 bootstrap_lookup_interval: u64,
574 bootstrap_lookup_countdown: u64,
575 metrics: Discv5Metrics,
576 discv5: Arc<discv5::Discv5>,
577) {
578 let local_node_id = discv5.local_enr().node_id();
579 let lookup_interval = Duration::from_secs(lookup_interval);
580 let metrics = metrics.discovered_peers;
581 let mut kbucket_index = MAX_KBUCKET_INDEX;
582 let pulse_lookup_interval = Duration::from_secs(bootstrap_lookup_interval);
583 task::spawn(Box::pin(async move {
584 for i in (0..bootstrap_lookup_countdown).rev() {
587 let target = discv5::enr::NodeId::random();
588
589 trace!(target: "net::discv5",
590 %target,
591 bootstrap_boost_runs_countdown=i,
592 lookup_interval=format!("{:#?}", pulse_lookup_interval),
593 "starting bootstrap boost lookup query"
594 );
595
596 lookup(target, &discv5, &metrics).await;
597
598 tokio::time::sleep(pulse_lookup_interval).await;
599 }
600
601 loop {
603 let target = get_lookup_target(kbucket_index, local_node_id);
606
607 trace!(target: "net::discv5",
608 %target,
609 lookup_interval=format!("{:#?}", lookup_interval),
610 "starting periodic lookup query"
611 );
612
613 lookup(target, &discv5, &metrics).await;
614
615 if kbucket_index > DEFAULT_MIN_TARGET_KBUCKET_INDEX {
616 kbucket_index -= 1
618 } else {
619 kbucket_index = MAX_KBUCKET_INDEX
621 }
622
623 tokio::time::sleep(lookup_interval).await;
624 }
625 }));
626}
627
628pub fn get_lookup_target(
630 kbucket_index: usize,
631 local_node_id: discv5::enr::NodeId,
632) -> discv5::enr::NodeId {
633 let mut target = local_node_id.raw();
635
636 let bit_offset = MAX_KBUCKET_INDEX.saturating_sub(kbucket_index);
638 let (byte, bit) = (bit_offset / 8, bit_offset % 8);
639 target[byte] ^= 1 << (7 - bit);
641
642 let mut rng = rand::rng();
644 if bit < 7 {
646 let bits_to_randomize = 0xff >> (bit + 1);
648 target[byte] &= !bits_to_randomize;
650 target[byte] |= rng.random::<u8>() & bits_to_randomize;
652 }
653 rng.fill_bytes(&mut target[byte + 1..]);
655
656 target.into()
657}
658
659pub async fn lookup(
661 target: discv5::enr::NodeId,
662 discv5: &discv5::Discv5,
663 metrics: &DiscoveredPeersMetrics,
664) {
665 metrics.set_total_sessions(discv5.metrics().active_sessions);
666 metrics.set_total_kbucket_peers(
667 discv5.with_kbuckets(|kbuckets| kbuckets.read().iter_ref().count()),
668 );
669
670 match discv5.find_node(target).await {
671 Err(err) => trace!(target: "net::discv5",
672 %err,
673 "lookup query failed"
674 ),
675 Ok(peers) => trace!(target: "net::discv5",
676 target=format!("{:#?}", target),
677 peers_count=peers.len(),
678 peers=format!("[{:#}]", peers.iter()
679 .map(|enr| enr.node_id()
680 ).format(", ")),
681 "peers returned by lookup query"
682 ),
683 }
684
685 debug!(target: "net::discv5",
689 connected_peers=discv5.connected_peers(),
690 "connected peers in routing table"
691 );
692}
693
694#[cfg(test)]
695mod test {
696 #![allow(deprecated)]
697 use super::*;
698 use ::enr::{CombinedKey, EnrKey};
699 use rand_08::thread_rng;
700 use reth_chainspec::MAINNET;
701 use reth_tracing::init_test_tracing;
702 use std::env;
703 use tracing::trace;
704
705 fn discv5_noop() -> Discv5 {
706 let sk = CombinedKey::generate_secp256k1();
707 Discv5 {
708 discv5: Arc::new(
709 discv5::Discv5::new(
710 Enr::empty(&sk).unwrap(),
711 sk,
712 discv5::ConfigBuilder::new(DEFAULT_DISCOVERY_V5_LISTEN_CONFIG).build(),
713 )
714 .unwrap(),
715 ),
716 rlpx_ip_mode: IpMode::Ip4,
717 fork_key: None,
718 discovered_peer_filter: MustNotIncludeKeys::default(),
719 metrics: Discv5Metrics::default(),
720 local_node_record: NodeRecord::new(
721 (Ipv4Addr::LOCALHOST, 30303).into(),
722 PeerId::random(),
723 ),
724 }
725 }
726
727 async fn start_discovery_node(udp_port_discv5: u16) -> (Discv5, mpsc::Receiver<discv5::Event>) {
728 let secret_key = SecretKey::new(&mut thread_rng());
729
730 let discv5_addr: SocketAddr = format!("127.0.0.1:{udp_port_discv5}").parse().unwrap();
731 let rlpx_addr: SocketAddr = "127.0.0.1:30303".parse().unwrap();
732
733 let discv5_listen_config = ListenConfig::from(discv5_addr);
734 let discv5_config = Config::builder(rlpx_addr)
735 .discv5_config(discv5::ConfigBuilder::new(discv5_listen_config).build())
736 .build();
737
738 Discv5::start(&secret_key, discv5_config).await.expect("should build discv5")
739 }
740
741 #[tokio::test(flavor = "multi_thread")]
742 async fn discv5() {
743 reth_tracing::init_test_tracing();
744
745 let (node_1, mut stream_1) = start_discovery_node(30344).await;
749 let node_1_enr = node_1.with_discv5(|discv5| discv5.local_enr());
750
751 let (node_2, mut stream_2) = start_discovery_node(30355).await;
753 let node_2_enr = node_2.with_discv5(|discv5| discv5.local_enr());
754
755 trace!(target: "net::discv5::test",
756 node_1_node_id=format!("{:#}", node_1_enr.node_id()),
757 node_2_node_id=format!("{:#}", node_2_enr.node_id()),
758 "started nodes"
759 );
760
761 let node_2_enr_reth_compatible_ty: Enr<SecretKey> =
765 EnrCombinedKeyWrapper(node_2_enr.clone()).into();
766 node_1.add_node(node_2_enr_reth_compatible_ty).unwrap();
767
768 assert!(
770 node_1.with_discv5(|discv5| discv5.table_entries_id().contains(&node_2_enr.node_id()))
771 );
772
773 node_1.with_discv5(|discv5| discv5.send_ping(node_2_enr.clone())).await.unwrap();
775
776 let event_1_v5 = stream_1.recv().await.unwrap();
778
779 assert!(matches!(
780 event_1_v5,
781 discv5::Event::SessionEstablished(node, socket) if node == node_2_enr && socket == node_2_enr.udp4_socket().unwrap().into()
782 ));
783
784 let event_2_v5 = stream_2.recv().await.unwrap();
786 assert!(matches!(
787 event_2_v5,
788 discv5::Event::NodeInserted { node_id, replaced } if node_id == node_1_enr.node_id() && replaced.is_none()
789 ));
790 }
791
792 #[test]
793 fn discovered_enr_disc_socket_missing() {
794 reth_tracing::init_test_tracing();
795
796 const REMOTE_RLPX_PORT: u16 = 30303;
798 let remote_socket = "104.28.44.25:9000".parse().unwrap();
799 let remote_key = CombinedKey::generate_secp256k1();
800 let remote_enr = Enr::builder().tcp4(REMOTE_RLPX_PORT).build(&remote_key).unwrap();
801
802 let discv5 = discv5_noop();
803
804 let filtered_peer = discv5.on_discovered_peer(&remote_enr, remote_socket);
806
807 assert_eq!(
808 NodeRecord {
809 address: remote_socket.ip(),
810 udp_port: remote_socket.port(),
811 tcp_port: REMOTE_RLPX_PORT,
812 id: enr_to_discv4_id(&remote_enr).unwrap(),
813 },
814 filtered_peer.unwrap().node_record
815 )
816 }
817
818 #[expect(unreachable_pub)]
821 #[expect(unused)]
822 mod sigp {
823 use alloy_primitives::U256;
824 use enr::{
825 k256::sha2::digest::generic_array::{typenum::U32, GenericArray},
826 NodeId,
827 };
828
829 #[derive(Clone, Debug)]
839 pub struct Key<T> {
840 preimage: T,
841 hash: GenericArray<u8, U32>,
842 }
843
844 impl<T> PartialEq for Key<T> {
845 fn eq(&self, other: &Self) -> bool {
846 self.hash == other.hash
847 }
848 }
849
850 impl<T> Eq for Key<T> {}
851
852 impl<TPeerId> AsRef<Self> for Key<TPeerId> {
853 fn as_ref(&self) -> &Self {
854 self
855 }
856 }
857
858 impl<T> Key<T> {
859 pub const fn new_raw(preimage: T, hash: GenericArray<u8, U32>) -> Self {
861 Self { preimage, hash }
862 }
863
864 pub const fn preimage(&self) -> &T {
866 &self.preimage
867 }
868
869 pub fn into_preimage(self) -> T {
871 self.preimage
872 }
873
874 pub fn distance<U>(&self, other: &Key<U>) -> Distance {
876 let a = U256::from_be_slice(self.hash.as_slice());
877 let b = U256::from_be_slice(other.hash.as_slice());
878 Distance(a ^ b)
879 }
880
881 pub fn log2_distance<U>(&self, other: &Key<U>) -> Option<u64> {
885 let xor_dist = self.distance(other);
886 let log_dist = (256 - xor_dist.0.leading_zeros() as u64);
887 (log_dist != 0).then_some(log_dist)
888 }
889 }
890
891 impl From<NodeId> for Key<NodeId> {
892 fn from(node_id: NodeId) -> Self {
893 Self { preimage: node_id, hash: *GenericArray::from_slice(&node_id.raw()) }
894 }
895 }
896
897 #[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Debug)]
899 pub struct Distance(pub(super) U256);
900 }
901
902 #[test]
903 fn select_lookup_target() {
904 for bucket_index in 0..=MAX_KBUCKET_INDEX {
905 let sk = CombinedKey::generate_secp256k1();
906 let local_node_id = discv5::enr::NodeId::from(sk.public());
907 let target = get_lookup_target(bucket_index, local_node_id);
908
909 let local_node_id = sigp::Key::from(local_node_id);
910 let target = sigp::Key::from(target);
911
912 assert_eq!(local_node_id.log2_distance(&target), Some(bucket_index as u64 + 1));
913 }
914 }
915
916 #[test]
917 fn build_enr_from_config() {
918 const TCP_PORT: u16 = 30303;
919 let fork_id = MAINNET.latest_fork_id();
920
921 let config = Config::builder((Ipv4Addr::UNSPECIFIED, TCP_PORT).into())
922 .fork(NetworkStackId::ETH, fork_id)
923 .build();
924
925 let sk = SecretKey::new(&mut thread_rng());
926 let (enr, _, _, _) = build_local_enr(&sk, &config);
927
928 let decoded_fork_id = enr
929 .get_decodable::<EnrForkIdEntry>(NetworkStackId::ETH)
930 .unwrap()
931 .map(Into::into)
932 .unwrap();
933
934 assert_eq!(fork_id, decoded_fork_id);
935 assert_eq!(TCP_PORT, enr.tcp4().unwrap()); }
937
938 #[test]
939 fn get_fork_id_with_different_network_stack_ids() {
940 unsafe {
941 env::set_var("RUST_LOG", "net::discv5=trace");
942 }
943 init_test_tracing();
944
945 let fork_id = MAINNET.latest_fork_id();
946 let sk = SecretKey::new(&mut thread_rng());
947
948 let enr_with_opel = Enr::builder()
950 .add_value_rlp(
951 NetworkStackId::OPEL,
952 alloy_rlp::encode(EnrForkIdEntry::from(fork_id)).into(),
953 )
954 .build(&sk)
955 .unwrap();
956
957 let mut discv5 = discv5_noop();
958 discv5.fork_key = Some(NetworkStackId::OPEL);
959 assert_eq!(discv5.get_fork_id(&enr_with_opel).unwrap(), fork_id);
960
961 let enr_with_eth = Enr::builder()
963 .add_value_rlp(
964 NetworkStackId::ETH,
965 alloy_rlp::encode(EnrForkIdEntry::from(fork_id)).into(),
966 )
967 .build(&sk)
968 .unwrap();
969
970 discv5.fork_key = Some(NetworkStackId::OPEL);
971 assert_eq!(discv5.get_fork_id(&enr_with_eth).unwrap(), fork_id);
972
973 let enr_without_network_stack_id = Enr::empty(&sk).unwrap();
975 discv5.fork_key = Some(NetworkStackId::OPEL);
976 assert!(matches!(
977 discv5.get_fork_id(&enr_without_network_stack_id),
978 Err(Error::ForkMissing(NetworkStackId::OPEL))
979 ));
980
981 let discv5 = discv5_noop();
983 assert!(matches!(
984 discv5.get_fork_id(&enr_without_network_stack_id),
985 Err(Error::NetworkStackIdNotConfigured)
986 ));
987 }
988}