1#![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#[derive(Clone, Debug)]
60pub struct DnsDiscoveryHandle {
61 to_service: UnboundedSender<DnsDiscoveryCommand>,
63}
64
65impl DnsDiscoveryHandle {
68 pub fn sync_tree(&self, link: &str) -> Result<(), ParseDnsEntryError> {
70 self.sync_tree_with_link(link.parse()?);
71 Ok(())
72 }
73
74 pub fn sync_tree_with_link(&self, link: LinkEntry) {
76 let _ = self.to_service.send(DnsDiscoveryCommand::SyncTree(link));
77 }
78
79 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#[must_use = "Service does nothing unless polled"]
92#[expect(missing_debug_implementations)]
93pub struct DnsDiscoveryService<R: Resolver = DnsResolver> {
94 command_tx: UnboundedSender<DnsDiscoveryCommand>,
96 command_rx: UnboundedReceiverStream<DnsDiscoveryCommand>,
98 node_record_listeners: Vec<mpsc::Sender<DnsNodeRecordUpdate>>,
100 trees: HashMap<LinkEntry, SyncTree>,
102 queries: QueryPool<R, SecretKey>,
104 dns_record_cache: LruMap<String, DnsEntry<SecretKey>>,
106 queued_events: VecDeque<DnsDiscoveryEvent>,
108 recheck_interval: Duration,
110 bootstrap_dns_networks: HashSet<LinkEntry>,
112}
113
114impl<R: Resolver> DnsDiscoveryService<R> {
117 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 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 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 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 pub fn handle(&self) -> DnsDiscoveryHandle {
182 DnsDiscoveryHandle { to_service: self.command_tx.clone() }
183 }
184
185 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 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 pub fn sync_tree(&mut self, link: &str) -> Result<(), ParseDnsEntryError> {
207 self.sync_tree_with_link(link.parse()?);
208 Ok(())
209 }
210
211 pub fn sync_tree_with_link(&mut self, link: LinkEntry) {
213 self.queries.resolve_root(link);
214 }
215
216 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 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 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 pub(crate) fn poll(&mut self, cx: &mut Context<'_>) -> Poll<DnsDiscoveryEvent> {
294 loop {
295 if let Some(event) = self.queued_events.pop_front() {
297 return Poll::Ready(event)
298 }
299
300 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 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
356impl<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#[derive(Debug, Clone, Eq, PartialEq)]
367pub struct DnsNodeRecordUpdate {
368 pub node_record: NodeRecord,
370 pub fork_id: Option<ForkId>,
372 pub enr: Enr<SecretKey>,
374}
375
376enum DnsDiscoveryCommand {
378 SyncTree(LinkEntry),
380 NodeRecordUpdates(oneshot::Sender<ReceiverStream<DnsNodeRecordUpdate>>),
381}
382
383#[derive(Debug, Clone)]
385pub enum DnsDiscoveryEvent {
386 Enr(Enr<SecretKey>),
388}
389
390fn convert_enr_node_record(enr: &Enr<SecretKey>) -> Option<DnsNodeRecordUpdate> {
392 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 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 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 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 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 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}