Skip to main content

reth_dns_discovery/
lib.rs

1//! Implementation of [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459) Node Discovery via DNS.
2//!
3//! ## Feature Flags
4//!
5//! - `serde` (default): Enable serde support
6//! - `test-utils`: Export utilities for testing
7
8#![doc(
9    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
10    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
11    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
12)]
13#![cfg_attr(not(test), warn(unused_crate_dependencies))]
14#![cfg_attr(docsrs, feature(doc_cfg))]
15
16pub use crate::resolver::{DnsResolver, MapResolver, Resolver};
17use crate::{
18    query::{QueryOutcome, QueryPool, ResolveEntryResult, ResolveRootResult},
19    sync::{ResolveKind, SyncAction},
20    tree::{DnsEntry, LinkEntry},
21};
22pub use config::DnsDiscoveryConfig;
23use enr::Enr;
24pub use error::ParseDnsEntryError;
25use reth_ethereum_forks::{EnrForkIdEntry, ForkId};
26use reth_network_peers::NodeRecord;
27use schnellru::{ByLength, LruMap};
28use secp256k1::SecretKey;
29use std::{
30    collections::{hash_map::Entry, HashMap, HashSet, VecDeque},
31    pin::Pin,
32    sync::Arc,
33    task::{ready, Context, Poll},
34    time::{Duration, Instant},
35};
36use sync::SyncTree;
37use tokio::{
38    sync::{
39        mpsc,
40        mpsc::{error::TrySendError, UnboundedSender},
41        oneshot,
42    },
43    task::JoinHandle,
44};
45use tokio_stream::{
46    wrappers::{ReceiverStream, UnboundedReceiverStream},
47    Stream, StreamExt,
48};
49use tracing::{debug, trace};
50
51mod config;
52mod error;
53mod query;
54pub mod resolver;
55mod sync;
56pub mod tree;
57
58/// [`DnsDiscoveryService`] front-end.
59#[derive(Clone, Debug)]
60pub struct DnsDiscoveryHandle {
61    /// Channel for sending commands to the service.
62    to_service: UnboundedSender<DnsDiscoveryCommand>,
63}
64
65// === impl DnsDiscovery ===
66
67impl DnsDiscoveryHandle {
68    /// Starts syncing the given link to a tree.
69    pub fn sync_tree(&self, link: &str) -> Result<(), ParseDnsEntryError> {
70        self.sync_tree_with_link(link.parse()?);
71        Ok(())
72    }
73
74    /// Starts syncing the given link to a tree.
75    pub fn sync_tree_with_link(&self, link: LinkEntry) {
76        let _ = self.to_service.send(DnsDiscoveryCommand::SyncTree(link));
77    }
78
79    /// Returns the receiver half of new listener channel that streams discovered [`NodeRecord`]s.
80    pub async fn node_record_stream(
81        &self,
82    ) -> Result<ReceiverStream<DnsNodeRecordUpdate>, oneshot::error::RecvError> {
83        let (tx, rx) = oneshot::channel();
84        let cmd = DnsDiscoveryCommand::NodeRecordUpdates(tx);
85        let _ = self.to_service.send(cmd);
86        rx.await
87    }
88}
89
90/// A client that discovers nodes via DNS.
91#[must_use = "Service does nothing unless polled"]
92#[expect(missing_debug_implementations)]
93pub struct DnsDiscoveryService<R: Resolver = DnsResolver> {
94    /// Copy of the sender half, so new [`DnsDiscoveryHandle`] can be created on demand.
95    command_tx: UnboundedSender<DnsDiscoveryCommand>,
96    /// Receiver half of the command channel.
97    command_rx: UnboundedReceiverStream<DnsDiscoveryCommand>,
98    /// All subscribers for resolved [`NodeRecord`]s.
99    node_record_listeners: Vec<mpsc::Sender<DnsNodeRecordUpdate>>,
100    /// All the trees that can be synced.
101    trees: HashMap<LinkEntry, SyncTree>,
102    /// All queries currently in progress
103    queries: QueryPool<R, SecretKey>,
104    /// Cached dns records
105    dns_record_cache: LruMap<String, DnsEntry<SecretKey>>,
106    /// all buffered events
107    queued_events: VecDeque<DnsDiscoveryEvent>,
108    /// The rate at which trees should be updated.
109    recheck_interval: Duration,
110    /// Links to the DNS networks to bootstrap.
111    bootstrap_dns_networks: HashSet<LinkEntry>,
112}
113
114// === impl DnsDiscoveryService ===
115
116impl<R: Resolver> DnsDiscoveryService<R> {
117    /// Creates a new instance of the [`DnsDiscoveryService`] using the given settings.
118    ///
119    /// ```
120    /// use reth_dns_discovery::{DnsDiscoveryService, DnsResolver};
121    /// use std::sync::Arc;
122    /// # fn t() {
123    /// let service = DnsDiscoveryService::new(
124    ///     Arc::new(DnsResolver::from_system_conf().unwrap()),
125    ///     Default::default(),
126    /// );
127    /// # }
128    /// ```
129    pub fn new(resolver: Arc<R>, config: DnsDiscoveryConfig) -> Self {
130        let DnsDiscoveryConfig {
131            lookup_timeout,
132            max_requests_per_sec,
133            recheck_interval,
134            dns_record_cache_limit,
135            bootstrap_dns_networks,
136        } = config;
137        let queries = QueryPool::new(resolver, max_requests_per_sec, lookup_timeout);
138        let (command_tx, command_rx) = mpsc::unbounded_channel();
139        Self {
140            command_tx,
141            command_rx: UnboundedReceiverStream::new(command_rx),
142            node_record_listeners: Default::default(),
143            trees: Default::default(),
144            queries,
145            dns_record_cache: LruMap::new(ByLength::new(dns_record_cache_limit.get())),
146            queued_events: Default::default(),
147            recheck_interval,
148            bootstrap_dns_networks: bootstrap_dns_networks.unwrap_or_default(),
149        }
150    }
151
152    /// Spawns this services onto a new task
153    ///
154    /// Note: requires a running runtime
155    pub fn spawn(mut self) -> JoinHandle<()> {
156        tokio::task::spawn(async move {
157            self.bootstrap();
158
159            while let Some(event) = self.next().await {
160                trace!(target: "disc::dns", ?event, "processed");
161            }
162        })
163    }
164
165    /// Starts discovery with all configured bootstrap links
166    pub fn bootstrap(&mut self) {
167        for link in self.bootstrap_dns_networks.clone() {
168            self.sync_tree_with_link(link);
169        }
170    }
171
172    /// Same as [`DnsDiscoveryService::new`] but also returns a new handle that's connected to the
173    /// service
174    pub fn new_pair(resolver: Arc<R>, config: DnsDiscoveryConfig) -> (Self, DnsDiscoveryHandle) {
175        let service = Self::new(resolver, config);
176        let handle = service.handle();
177        (service, handle)
178    }
179
180    /// Returns a new [`DnsDiscoveryHandle`] that can send commands to this type.
181    pub fn handle(&self) -> DnsDiscoveryHandle {
182        DnsDiscoveryHandle { to_service: self.command_tx.clone() }
183    }
184
185    /// Creates a new channel for [`NodeRecord`]s.
186    pub fn node_record_stream(&mut self) -> ReceiverStream<DnsNodeRecordUpdate> {
187        let (tx, rx) = mpsc::channel(256);
188        self.node_record_listeners.push(tx);
189        ReceiverStream::new(rx)
190    }
191
192    /// Sends  the event to all listeners.
193    ///
194    /// Remove channels that got closed.
195    fn notify(&mut self, record: DnsNodeRecordUpdate) {
196        self.node_record_listeners.retain_mut(|listener| match listener.try_send(record.clone()) {
197            Ok(()) => true,
198            Err(err) => match err {
199                TrySendError::Full(_) => true,
200                TrySendError::Closed(_) => false,
201            },
202        });
203    }
204
205    /// Starts syncing the given link to a tree.
206    pub fn sync_tree(&mut self, link: &str) -> Result<(), ParseDnsEntryError> {
207        self.sync_tree_with_link(link.parse()?);
208        Ok(())
209    }
210
211    /// Starts syncing the given link to a tree.
212    pub fn sync_tree_with_link(&mut self, link: LinkEntry) {
213        self.queries.resolve_root(link);
214    }
215
216    /// Resolves an entry
217    fn resolve_entry(&mut self, link: LinkEntry<SecretKey>, hash: String, kind: ResolveKind) {
218        if let Some(entry) = self.dns_record_cache.get(&hash).cloned() {
219            // already resolved
220            let cached = ResolveEntryResult { entry: Some(Ok(entry)), link, hash, kind };
221            self.on_resolved_entry(cached);
222            return
223        }
224        self.queries.resolve_entry(link, hash, kind)
225    }
226
227    fn on_resolved_root(&mut self, resp: ResolveRootResult<SecretKey>) {
228        match resp {
229            Ok((root, link)) => match self.trees.entry(link.clone()) {
230                Entry::Occupied(mut entry) => {
231                    entry.get_mut().update_root(root);
232                }
233                Entry::Vacant(entry) => {
234                    entry.insert(SyncTree::new(root, link));
235                }
236            },
237            Err((err, link)) => {
238                debug!(target: "disc::dns",%err, ?link, "Failed to lookup root")
239            }
240        }
241    }
242
243    fn on_resolved_enr(&mut self, enr: Enr<SecretKey>) {
244        if let Some(record) = convert_enr_node_record(&enr) {
245            self.notify(record);
246        }
247        self.queued_events.push_back(DnsDiscoveryEvent::Enr(enr))
248    }
249
250    fn on_resolved_entry(&mut self, resp: ResolveEntryResult<SecretKey>) {
251        let ResolveEntryResult { entry, link, hash, kind } = resp;
252
253        match entry {
254            Some(Err(err)) => {
255                debug!(target: "disc::dns",%err, domain=%link.domain, ?hash, "Failed to lookup entry")
256            }
257            None => {
258                trace!(target: "disc::dns",domain=%link.domain, ?hash, "No dns entry")
259            }
260            Some(Ok(entry)) => {
261                // cache entry
262                self.dns_record_cache.insert(hash.clone(), entry.clone());
263
264                match entry {
265                    DnsEntry::Root(root) => {
266                        debug!(target: "disc::dns",%root, domain=%link.domain, ?hash, "resolved unexpected root entry");
267                    }
268                    DnsEntry::Link(link_entry) => {
269                        if kind.is_link() {
270                            self.sync_tree_with_link(link_entry)
271                        } else {
272                            debug!(target: "disc::dns",%link_entry, domain=%link.domain, ?hash, "resolved unexpected Link entry");
273                        }
274                    }
275                    DnsEntry::Branch(branch_entry) => {
276                        if let Some(tree) = self.trees.get_mut(&link) {
277                            tree.extend_children(kind, branch_entry.children)
278                        }
279                    }
280                    DnsEntry::Node(entry) => {
281                        if kind.is_link() {
282                            debug!(target: "disc::dns",domain=%link.domain, ?hash, "resolved unexpected enr entry");
283                        } else {
284                            self.on_resolved_enr(entry.enr)
285                        }
286                    }
287                }
288            }
289        }
290    }
291
292    /// Advances the state of the DNS discovery service by polling,triggering lookups
293    pub(crate) fn poll(&mut self, cx: &mut Context<'_>) -> Poll<DnsDiscoveryEvent> {
294        loop {
295            // drain buffered events first
296            if let Some(event) = self.queued_events.pop_front() {
297                return Poll::Ready(event)
298            }
299
300            // process all incoming commands
301            while let Poll::Ready(Some(cmd)) = Pin::new(&mut self.command_rx).poll_next(cx) {
302                match cmd {
303                    DnsDiscoveryCommand::SyncTree(link) => {
304                        self.sync_tree_with_link(link);
305                    }
306                    DnsDiscoveryCommand::NodeRecordUpdates(tx) => {
307                        let _ = tx.send(self.node_record_stream());
308                    }
309                }
310            }
311
312            while let Poll::Ready(outcome) = self.queries.poll(cx) {
313                // handle query outcome
314                match outcome {
315                    QueryOutcome::Root(resp) => self.on_resolved_root(resp),
316                    QueryOutcome::Entry(resp) => self.on_resolved_entry(resp),
317                }
318            }
319
320            let mut progress = false;
321            let now = Instant::now();
322            let mut pending_resolves = Vec::new();
323            let mut pending_updates = Vec::new();
324            for tree in self.trees.values_mut() {
325                while let Some(action) = tree.poll(now, self.recheck_interval) {
326                    progress = true;
327                    match action {
328                        SyncAction::UpdateRoot => {
329                            pending_updates.push(tree.link().clone());
330                        }
331                        SyncAction::Enr(hash) => {
332                            pending_resolves.push((tree.link().clone(), hash, ResolveKind::Enr));
333                        }
334                        SyncAction::Link(hash) => {
335                            pending_resolves.push((tree.link().clone(), hash, ResolveKind::Link));
336                        }
337                    }
338                }
339            }
340
341            for (domain, hash, kind) in pending_resolves {
342                self.resolve_entry(domain, hash, kind)
343            }
344
345            for link in pending_updates {
346                self.sync_tree_with_link(link)
347            }
348
349            if !progress && self.queued_events.is_empty() {
350                return Poll::Pending
351            }
352        }
353    }
354}
355
356/// A Stream events, mainly used for debugging
357impl<R: Resolver> Stream for DnsDiscoveryService<R> {
358    type Item = DnsDiscoveryEvent;
359
360    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
361        Poll::Ready(Some(ready!(self.get_mut().poll(cx))))
362    }
363}
364
365/// The converted discovered [Enr] object
366#[derive(Debug, Clone, Eq, PartialEq)]
367pub struct DnsNodeRecordUpdate {
368    /// Discovered node and it's addresses
369    pub node_record: NodeRecord,
370    /// The forkid of the node, if present in the ENR
371    pub fork_id: Option<ForkId>,
372    /// Original [`Enr`].
373    pub enr: Enr<SecretKey>,
374}
375
376/// Commands sent from [`DnsDiscoveryHandle`] to [`DnsDiscoveryService`]
377enum DnsDiscoveryCommand {
378    /// Sync a tree
379    SyncTree(LinkEntry),
380    NodeRecordUpdates(oneshot::Sender<ReceiverStream<DnsNodeRecordUpdate>>),
381}
382
383/// Represents dns discovery related update events.
384#[derive(Debug, Clone)]
385pub enum DnsDiscoveryEvent {
386    /// Resolved an Enr entry via DNS.
387    Enr(Enr<SecretKey>),
388}
389
390/// Converts an [Enr] into a [`NodeRecord`]
391fn convert_enr_node_record(enr: &Enr<SecretKey>) -> Option<DnsNodeRecordUpdate> {
392    // DNS discovery yields RLPx dial targets, so records without a tcp endpoint are skipped.
393    let node_record = NodeRecord::try_from(enr).ok().filter(NodeRecord::has_rlpx_endpoint)?;
394
395    let fork_id =
396        enr.get_decodable::<EnrForkIdEntry>(b"eth").transpose().ok().flatten().map(Into::into);
397
398    Some(DnsNodeRecordUpdate { node_record, fork_id, enr: enr.clone() })
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use crate::tree::TreeRootEntry;
405    use alloy_chains::Chain;
406    use alloy_primitives::keccak256;
407    use alloy_rlp::{Decodable, Encodable};
408    use data_encoding::BASE32_NOPAD;
409    use enr::EnrKey;
410    use reth_chainspec::MAINNET;
411    use reth_ethereum_forks::{EthereumHardfork, ForkHash};
412    use secp256k1::rand::thread_rng;
413    use std::{
414        future::poll_fn,
415        net::{IpAddr, Ipv4Addr},
416    };
417
418    fn entry_hash(entry_txt: &str) -> String {
419        BASE32_NOPAD.encode(&keccak256(entry_txt.as_bytes()).as_slice()[..16])
420    }
421
422    #[test]
423    fn test_convert_enr_node_record() {
424        // rig
425        let secret_key = SecretKey::new(&mut secp256k1::rand::thread_rng());
426        let enr = Enr::builder()
427            .ip("127.0.0.1".parse().unwrap())
428            .udp4(9000)
429            .tcp4(30303)
430            .add_value(b"eth", &EnrForkIdEntry::from(MAINNET.latest_fork_id()))
431            .build(&secret_key)
432            .unwrap();
433
434        // test
435        let node_record_update = convert_enr_node_record(&enr).unwrap();
436
437        assert_eq!(node_record_update.node_record.address, "127.0.0.1".parse::<IpAddr>().unwrap());
438        assert_eq!(node_record_update.node_record.tcp_port, 30303);
439        assert_eq!(node_record_update.node_record.udp_port, 9000);
440        assert_eq!(node_record_update.fork_id, Some(MAINNET.latest_fork_id()));
441        assert_eq!(node_record_update.enr, enr);
442    }
443
444    #[test]
445    fn test_decode_and_convert_enr_node_record() {
446        // rig
447
448        let secret_key = SecretKey::new(&mut secp256k1::rand::thread_rng());
449        let enr = Enr::builder()
450            .ip("127.0.0.1".parse().unwrap())
451            .udp4(9000)
452            .tcp4(30303)
453            .add_value(b"eth", &EnrForkIdEntry::from(MAINNET.latest_fork_id()))
454            .add_value(b"opstack", &ForkId { hash: ForkHash(rand::random()), next: rand::random() })
455            .build(&secret_key)
456            .unwrap();
457
458        let mut encoded_enr = vec![];
459        enr.encode(&mut encoded_enr);
460
461        // test
462        let decoded_enr = Enr::decode(&mut &encoded_enr[..]).unwrap();
463
464        let node_record_update = convert_enr_node_record(&decoded_enr).unwrap();
465
466        assert_eq!(node_record_update.node_record.address, "127.0.0.1".parse::<IpAddr>().unwrap());
467        assert_eq!(node_record_update.node_record.tcp_port, 30303);
468        assert_eq!(node_record_update.node_record.udp_port, 9000);
469        assert_eq!(node_record_update.fork_id, Some(MAINNET.latest_fork_id()));
470        assert_eq!(node_record_update.enr, enr);
471    }
472
473    #[tokio::test]
474    async fn test_start_root_sync() {
475        reth_tracing::init_test_tracing();
476
477        let secret_key = SecretKey::new(&mut thread_rng());
478        let resolver = MapResolver::default();
479        let s = "enrtree-root:v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE";
480        let mut root: TreeRootEntry = s.parse().unwrap();
481        root.sign(&secret_key).unwrap();
482
483        let link =
484            LinkEntry { domain: "nodes.example.org".to_string(), pubkey: secret_key.public() };
485        resolver.insert(link.domain.clone(), root.to_string());
486
487        let mut service = DnsDiscoveryService::new(Arc::new(resolver), Default::default());
488
489        service.sync_tree_with_link(link.clone());
490
491        poll_fn(|cx| {
492            let _ = service.poll(cx);
493            Poll::Ready(())
494        })
495        .await;
496
497        let tree = service.trees.get(&link).unwrap();
498        assert_eq!(tree.root().clone(), root);
499    }
500
501    #[tokio::test(flavor = "multi_thread")]
502    async fn test_get_node() {
503        reth_tracing::init_test_tracing();
504
505        let secret_key = SecretKey::new(&mut thread_rng());
506        let resolver = MapResolver::default();
507        let s = "enrtree-root:v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE";
508        let mut root: TreeRootEntry = s.parse().unwrap();
509
510        let link =
511            LinkEntry { domain: "nodes.example.org".to_string(), pubkey: secret_key.public() };
512
513        let mut builder = Enr::builder();
514        let fork_id = MAINNET.hardfork_fork_id(EthereumHardfork::Frontier).unwrap();
515        builder
516            .ip4(Ipv4Addr::LOCALHOST)
517            .udp4(30303)
518            .tcp4(30303)
519            .add_value(b"eth", &EnrForkIdEntry::from(fork_id));
520        let enr = builder.build(&secret_key).unwrap();
521        let enr_txt = enr.to_base64();
522
523        root.enr_root = entry_hash(&enr_txt);
524        root.sign(&secret_key).unwrap();
525
526        resolver.insert(link.domain.clone(), root.to_string());
527        resolver.insert(format!("{}.{}", root.enr_root.clone(), link.domain), enr_txt);
528
529        let mut service = DnsDiscoveryService::new(Arc::new(resolver), Default::default());
530
531        let mut node_records = service.node_record_stream();
532
533        let task = tokio::task::spawn(async move {
534            let record = node_records.next().await.unwrap();
535            assert_eq!(record.fork_id, Some(fork_id));
536        });
537
538        service.sync_tree_with_link(link.clone());
539
540        let event = poll_fn(|cx| service.poll(cx)).await;
541
542        match event {
543            DnsDiscoveryEvent::Enr(discovered) => {
544                assert_eq!(discovered, enr);
545            }
546        }
547
548        poll_fn(|cx| {
549            assert!(service.poll(cx).is_pending());
550            Poll::Ready(())
551        })
552        .await;
553
554        task.await.unwrap();
555    }
556
557    #[tokio::test]
558    async fn test_recheck_tree() {
559        reth_tracing::init_test_tracing();
560
561        let config = DnsDiscoveryConfig {
562            recheck_interval: Duration::from_millis(750),
563            ..Default::default()
564        };
565
566        let secret_key = SecretKey::new(&mut thread_rng());
567        let resolver = Arc::new(MapResolver::default());
568        let s = "enrtree-root:v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE";
569        let mut root: TreeRootEntry = s.parse().unwrap();
570        root.sign(&secret_key).unwrap();
571
572        let link =
573            LinkEntry { domain: "nodes.example.org".to_string(), pubkey: secret_key.public() };
574        resolver.insert(link.domain.clone(), root.to_string());
575
576        let mut service = DnsDiscoveryService::new(Arc::clone(&resolver), config.clone());
577
578        service.sync_tree_with_link(link.clone());
579
580        poll_fn(|cx| {
581            assert!(service.poll(cx).is_pending());
582            Poll::Ready(())
583        })
584        .await;
585
586        // await recheck timeout
587        tokio::time::sleep(config.recheck_interval).await;
588
589        let mut new_root = root.clone();
590        new_root.sequence_number = new_root.sequence_number.saturating_add(1);
591
592        let enr = Enr::empty(&secret_key).unwrap();
593        let enr_txt = enr.to_base64();
594        new_root.enr_root = entry_hash(&enr_txt);
595        new_root.sign(&secret_key).unwrap();
596        resolver.insert(link.domain.clone(), new_root.to_string());
597        resolver.insert(format!("{}.{}", new_root.enr_root.clone(), link.domain), enr_txt);
598
599        let event = poll_fn(|cx| service.poll(cx)).await;
600
601        match event {
602            DnsDiscoveryEvent::Enr(discovered) => {
603                assert_eq!(discovered, enr);
604            }
605        }
606
607        poll_fn(|cx| {
608            assert!(service.poll(cx).is_pending());
609            Poll::Ready(())
610        })
611        .await;
612    }
613
614    #[tokio::test]
615    async fn test_hash_mismatch_is_not_cached_and_does_not_poison_same_hash() {
616        let secret_key = SecretKey::new(&mut thread_rng());
617        let resolver = MapResolver::default();
618
619        let invalid_entry = "enrtree-branch:AAAAAAAAAAAAAAAAAAAA".to_string();
620        let valid_entry = "enrtree-branch:YNEGZIWHOM7TOOSUATAPTM".to_string();
621
622        let hash = entry_hash(&valid_entry);
623
624        let bad_link =
625            LinkEntry { domain: "bad.example.org".to_string(), pubkey: secret_key.public() };
626        let good_link =
627            LinkEntry { domain: "good.example.org".to_string(), pubkey: secret_key.public() };
628
629        resolver.insert(format!("{}.{}", hash, bad_link.domain), invalid_entry);
630        resolver.insert(format!("{}.{}", hash, good_link.domain), valid_entry.clone());
631
632        let mut service = DnsDiscoveryService::new(Arc::new(resolver), Default::default());
633
634        service.resolve_entry(bad_link, hash.clone(), ResolveKind::Enr);
635        poll_fn(|cx| {
636            let _ = service.poll(cx);
637            Poll::Ready(())
638        })
639        .await;
640
641        assert!(service.dns_record_cache.get(&hash).is_none());
642
643        service.resolve_entry(good_link, hash.clone(), ResolveKind::Enr);
644        poll_fn(|cx| {
645            let _ = service.poll(cx);
646            Poll::Ready(())
647        })
648        .await;
649
650        let cached = service.dns_record_cache.get(&hash).cloned();
651        assert_eq!(cached.map(|entry| entry.to_string()), Some(valid_entry));
652    }
653
654    #[tokio::test]
655    #[ignore]
656    async fn test_dns_resolver() {
657        reth_tracing::init_test_tracing();
658
659        let mut service = DnsDiscoveryService::new(
660            Arc::new(DnsResolver::from_system_conf().unwrap()),
661            Default::default(),
662        );
663
664        service.sync_tree(&Chain::mainnet().public_dns_network_protocol().unwrap()).unwrap();
665
666        while let Some(event) = service.next().await {
667            match event {
668                DnsDiscoveryEvent::Enr(enr) => {
669                    println!("discovered enr {}", enr.to_base64());
670                }
671            }
672        }
673    }
674}