mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat(dns): add dns discovery service (#768)
* feat(dns): add dns discovery service * feat: add entry types * add codec impls * resolve basics * Update crates/net/dns/src/tree.rs Co-authored-by: Bjerg <onbjerg@users.noreply.github.com> Co-authored-by: Bjerg <onbjerg@users.noreply.github.com>
This commit is contained in:
138
Cargo.lock
generated
138
Cargo.lock
generated
@ -153,9 +153,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.60"
|
version = "0.1.61"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3"
|
checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1277,6 +1277,18 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-as-inner"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-ordinalize"
|
name = "enum-ordinalize"
|
||||||
version = "3.1.12"
|
version = "3.1.12"
|
||||||
@ -1912,6 +1924,17 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"match_cfg",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
@ -2167,6 +2190,18 @@ dependencies = [
|
|||||||
"windows-sys 0.42.0",
|
"windows-sys 0.42.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipconfig"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be"
|
||||||
|
dependencies = [
|
||||||
|
"socket2",
|
||||||
|
"widestring",
|
||||||
|
"winapi",
|
||||||
|
"winreg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@ -2500,6 +2535,15 @@ dependencies = [
|
|||||||
"hashbrown 0.12.3",
|
"hashbrown 0.12.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-cache"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
|
||||||
|
dependencies = [
|
||||||
|
"linked-hash-map",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mach"
|
name = "mach"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -2509,6 +2553,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "match_cfg"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -3211,7 +3261,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-futures",
|
"tracing-futures",
|
||||||
"trust-dns-client",
|
"trust-dns-client",
|
||||||
"trust-dns-proto",
|
"trust-dns-proto 0.20.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3467,6 +3517,16 @@ dependencies = [
|
|||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "resolv-conf"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
|
||||||
|
dependencies = [
|
||||||
|
"hostname",
|
||||||
|
"quick-error 1.2.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reth"
|
name = "reth"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -3595,6 +3655,23 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reth-dns-discovery"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"enr 0.7.0",
|
||||||
|
"reth-primitives",
|
||||||
|
"secp256k1 0.24.2",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tracing",
|
||||||
|
"trust-dns-resolver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reth-downloaders"
|
name = "reth-downloaders"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -5298,7 +5375,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"trust-dns-proto",
|
"trust-dns-proto 0.20.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5310,7 +5387,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"enum-as-inner",
|
"enum-as-inner 0.3.4",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -5326,6 +5403,51 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trust-dns-proto"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"cfg-if",
|
||||||
|
"data-encoding",
|
||||||
|
"enum-as-inner 0.5.1",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"idna 0.2.3",
|
||||||
|
"ipnet",
|
||||||
|
"lazy_static",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror",
|
||||||
|
"tinyvec",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trust-dns-resolver"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"ipconfig",
|
||||||
|
"lazy_static",
|
||||||
|
"lru-cache",
|
||||||
|
"parking_lot 0.12.1",
|
||||||
|
"resolv-conf",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"trust-dns-proto 0.22.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@ -5618,6 +5740,12 @@ dependencies = [
|
|||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wildmatch"
|
name = "wildmatch"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|||||||
@ -11,6 +11,7 @@ members = [
|
|||||||
"crates/net/ecies",
|
"crates/net/ecies",
|
||||||
"crates/net/eth-wire",
|
"crates/net/eth-wire",
|
||||||
"crates/net/discv4",
|
"crates/net/discv4",
|
||||||
|
"crates/net/dns",
|
||||||
"crates/net/nat",
|
"crates/net/nat",
|
||||||
"crates/net/network",
|
"crates/net/network",
|
||||||
"crates/net/ipc",
|
"crates/net/ipc",
|
||||||
|
|||||||
34
crates/net/dns/Cargo.toml
Normal file
34
crates/net/dns/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "reth-dns-discovery"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/paradigmxyz/reth"
|
||||||
|
readme = "README.md"
|
||||||
|
description = "Support for EIP-1459 Node Discovery via DNS"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# reth
|
||||||
|
reth-primitives = { path = "../../primitives" }
|
||||||
|
|
||||||
|
# ethereum
|
||||||
|
secp256k1 = { version = "0.24", features = [
|
||||||
|
"global-context",
|
||||||
|
"rand-std",
|
||||||
|
"recovery",
|
||||||
|
] }
|
||||||
|
enr = { version = "0.7.0", default-features = false, features = ["rust-secp256k1"] }
|
||||||
|
|
||||||
|
# async/futures
|
||||||
|
tokio = { version = "1", features = ["io-util", "net", "time"] }
|
||||||
|
tokio-stream = "0.1"
|
||||||
|
|
||||||
|
# trust-dns
|
||||||
|
trust-dns-resolver = "0.22"
|
||||||
|
|
||||||
|
# misc
|
||||||
|
data-encoding = "2"
|
||||||
|
bytes = "1.2"
|
||||||
|
tracing = "0.1"
|
||||||
|
thiserror = "1.0"
|
||||||
|
async-trait = "0.1.61"
|
||||||
23
crates/net/dns/src/config.rs
Normal file
23
crates/net/dns/src/config.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Settings for the [DnsDiscoveryClient](crate::DnsDiscoveryClient).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DnsDiscoveryConfig {
|
||||||
|
/// Timeout for DNS lookups.
|
||||||
|
///
|
||||||
|
/// Default: 5s
|
||||||
|
pub lookup_timeout: Duration,
|
||||||
|
/// The rate at which lookups should be re-triggered.
|
||||||
|
///
|
||||||
|
/// Default: 30min
|
||||||
|
pub lookup_interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DnsDiscoveryConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
lookup_timeout: Duration::from_secs(5),
|
||||||
|
lookup_interval: Duration::from_secs(60 * 30),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
crates/net/dns/src/lib.rs
Normal file
110
crates/net/dns/src/lib.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#![warn(missing_docs, unreachable_pub)]
|
||||||
|
#![deny(unused_must_use, rust_2018_idioms)]
|
||||||
|
#![doc(test(
|
||||||
|
no_crate_inject,
|
||||||
|
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
|
||||||
|
))]
|
||||||
|
// TODO rm later
|
||||||
|
#![allow(missing_docs, unreachable_pub, unused)]
|
||||||
|
|
||||||
|
//! Implementation of [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459) Node Discovery via DNS.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::Arc,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
use tokio::sync::{mpsc, mpsc::UnboundedSender};
|
||||||
|
use tokio_stream::wrappers::{ReceiverStream, UnboundedReceiverStream};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
pub mod resolver;
|
||||||
|
mod sync;
|
||||||
|
pub mod tree;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
sync::SyncTree,
|
||||||
|
tree::{LinkEntry, ParseDnsEntryError},
|
||||||
|
};
|
||||||
|
pub use config::DnsDiscoveryConfig;
|
||||||
|
|
||||||
|
/// [DnsDiscoveryService] front-end.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DnsDiscoveryHandle {
|
||||||
|
/// Channel for sending commands to the service.
|
||||||
|
to_service: UnboundedSender<DnsDiscoveryCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === impl DnsDiscovery ===
|
||||||
|
|
||||||
|
impl DnsDiscoveryHandle {}
|
||||||
|
|
||||||
|
/// A client that discovers nodes via DNS.
|
||||||
|
#[must_use = "Service does nothing unless polled"]
|
||||||
|
pub struct DnsDiscoveryService {
|
||||||
|
/// Copy of the sender half, so new [`DnsDiscoveryHandle`] can be created on demand.
|
||||||
|
command_tx: UnboundedSender<DnsDiscoveryCommand>,
|
||||||
|
/// Receiver half of the command channel.
|
||||||
|
command_rx: UnboundedReceiverStream<DnsDiscoveryCommand>,
|
||||||
|
/// All subscribers for event updates.
|
||||||
|
event_listener: Vec<mpsc::Sender<DnsDiscoveryEvent>>,
|
||||||
|
/// All the trees that can be synced.
|
||||||
|
trees: HashMap<Arc<LinkEntry>, SyncTree>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === impl DnsDiscoveryService ===
|
||||||
|
|
||||||
|
impl DnsDiscoveryService {
|
||||||
|
/// Creates a new instance of the [DnsDiscoveryService] using the given settings.
|
||||||
|
pub fn new(_config: DnsDiscoveryConfig) -> Self {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [DnsDiscoveryService::new] but also returns a new handle that's connected to the
|
||||||
|
/// service
|
||||||
|
pub fn new_pair(config: DnsDiscoveryConfig) -> (Self, DnsDiscoveryHandle) {
|
||||||
|
let service = Self::new(config);
|
||||||
|
let handle = service.handle();
|
||||||
|
(service, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [`DnsDiscoveryHandle`] that can send commands to this type.
|
||||||
|
pub fn handle(&self) -> DnsDiscoveryHandle {
|
||||||
|
DnsDiscoveryHandle { to_service: self.command_tx.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new channel for [`DiscoveryUpdate`]s.
|
||||||
|
pub fn event_listener(&mut self) -> ReceiverStream<DnsDiscoveryEvent> {
|
||||||
|
let (tx, rx) = mpsc::channel(256);
|
||||||
|
self.event_listener.push(tx);
|
||||||
|
ReceiverStream::new(rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends the event to all listeners.
|
||||||
|
///
|
||||||
|
/// Remove channels that got closed.
|
||||||
|
fn notify(&mut self, event: DnsDiscoveryEvent) {
|
||||||
|
self.event_listener.retain(|listener| listener.try_send(event.clone()).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts syncing the given link to a tree.
|
||||||
|
pub fn sync_tree(&mut self, link: &str) -> Result<(), ParseDnsEntryError> {
|
||||||
|
let _link: LinkEntry = link.parse()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves an entry
|
||||||
|
fn resolve_entry(&mut self, _domain: impl Into<String>, _hash: impl Into<String>) {}
|
||||||
|
|
||||||
|
/// Advances the state of the DNS discovery service by polling,triggering lookups
|
||||||
|
pub(crate) fn poll(&mut self, _cx: &mut Context<'_>) -> Poll<()> {
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DnsDiscoveryCommand {}
|
||||||
|
|
||||||
|
/// Represents dns discovery related update events.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DnsDiscoveryEvent {}
|
||||||
39
crates/net/dns/src/resolver.rs
Normal file
39
crates/net/dns/src/resolver.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//! Perform DNS lookups
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A type that can lookup DNS entries
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Resolver: Send + Sync {
|
||||||
|
/// Performs a textual lookup.
|
||||||
|
async fn lookup_txt(&self, query: &str) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [Resolver] that uses an in memory map to lookup entries
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MapResolver(HashMap<String, String>);
|
||||||
|
|
||||||
|
impl Deref for MapResolver {
|
||||||
|
type Target = HashMap<String, String>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for MapResolver {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Resolver for MapResolver {
|
||||||
|
async fn lookup_txt(&self, query: &str) -> Option<String> {
|
||||||
|
self.get(query).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/net/dns/src/sync.rs
Normal file
11
crates/net/dns/src/sync.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//! Sync trees
|
||||||
|
|
||||||
|
use crate::tree::LinkEntry;
|
||||||
|
use enr::EnrKeyUnambiguous;
|
||||||
|
use secp256k1::SecretKey;
|
||||||
|
|
||||||
|
/// A sync-able tree
|
||||||
|
pub(crate) struct SyncTree<K: EnrKeyUnambiguous = SecretKey> {
|
||||||
|
/// The link to this tree.
|
||||||
|
link: LinkEntry<K>,
|
||||||
|
}
|
||||||
384
crates/net/dns/src/tree.rs
Normal file
384
crates/net/dns/src/tree.rs
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
//! Support for the [EIP-1459 DNS Record Structure](https://eips.ethereum.org/EIPS/eip-1459#dns-record-structure)
|
||||||
|
//!
|
||||||
|
//! The nodes in a list are encoded as a merkle tree for distribution via the DNS protocol. Entries
|
||||||
|
//! of the merkle tree are contained in DNS TXT records. The root of the tree is a TXT record with
|
||||||
|
//! the following content:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! enrtree-root:v1 e=<enr-root> l=<link-root> seq=<sequence-number> sig=<signature>
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! where
|
||||||
|
//!
|
||||||
|
//! enr-root and link-root refer to the root hashes of subtrees containing nodes and links to
|
||||||
|
//! subtrees.
|
||||||
|
//! `sequence-number` is the tree’s update sequence number, a decimal integer.
|
||||||
|
//! `signature` is a 65-byte secp256k1 EC signature over the keccak256 hash of the record
|
||||||
|
//! content, excluding the sig= part, encoded as URL-safe base64 (RFC-4648).
|
||||||
|
|
||||||
|
use crate::tree::ParseDnsEntryError::{FieldNotFound, UnknownEntry};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD};
|
||||||
|
use enr::{Enr, EnrKey, EnrKeyUnambiguous, EnrPublicKey};
|
||||||
|
use reth_primitives::hex;
|
||||||
|
use secp256k1::SecretKey;
|
||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
const ROOT_V1_PREFIX: &str = "enrtree-root:v1";
|
||||||
|
const LINK_PREFIX: &str = "enrtree://";
|
||||||
|
const BRANCH_PREFIX: &str = "enrtree-branch:";
|
||||||
|
const ENR_PREFIX: &str = "enr:";
|
||||||
|
|
||||||
|
/// Represents all variants
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum DnsEntry<K: EnrKeyUnambiguous> {
|
||||||
|
Root(TreeRootEntry),
|
||||||
|
Link(LinkEntry<K>),
|
||||||
|
Branch(BranchEntry),
|
||||||
|
Node(NodeEntry<K>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: EnrKeyUnambiguous> fmt::Display for DnsEntry<K> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
DnsEntry::Root(entry) => entry.fmt(f),
|
||||||
|
DnsEntry::Link(entry) => entry.fmt(f),
|
||||||
|
DnsEntry::Branch(entry) => entry.fmt(f),
|
||||||
|
DnsEntry::Node(entry) => entry.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error while parsing a [DnsEntry]
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum ParseDnsEntryError {
|
||||||
|
#[error("Unknown entry: {0}")]
|
||||||
|
UnknownEntry(String),
|
||||||
|
#[error("Field {0} not found.")]
|
||||||
|
FieldNotFound(&'static str),
|
||||||
|
#[error("Base64 decoding failed: {0}")]
|
||||||
|
Base64DecodeError(String),
|
||||||
|
#[error("Base32 decoding failed: {0}")]
|
||||||
|
Base32DecodeError(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
RlpDecodeError(String),
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: EnrKeyUnambiguous> FromStr for DnsEntry<K> {
|
||||||
|
type Err = ParseDnsEntryError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Some(s) = s.strip_prefix(ROOT_V1_PREFIX) {
|
||||||
|
TreeRootEntry::parse_value(s).map(DnsEntry::Root)
|
||||||
|
} else if let Some(s) = s.strip_prefix(BRANCH_PREFIX) {
|
||||||
|
BranchEntry::parse_value(s).map(DnsEntry::Branch)
|
||||||
|
} else if let Some(s) = s.strip_prefix(LINK_PREFIX) {
|
||||||
|
LinkEntry::parse_value(s).map(DnsEntry::Link)
|
||||||
|
} else if let Some(s) = s.strip_prefix(ENR_PREFIX) {
|
||||||
|
NodeEntry::parse_value(s).map(DnsEntry::Node)
|
||||||
|
} else {
|
||||||
|
Err(UnknownEntry(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an `enr-root` hash of subtrees containing nodes and links.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TreeRootEntry {
|
||||||
|
enr_root: String,
|
||||||
|
link_root: String,
|
||||||
|
sequence_number: u64,
|
||||||
|
signature: Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === impl TreeRootEntry ===
|
||||||
|
|
||||||
|
impl TreeRootEntry {
|
||||||
|
/// Parses the entry from text.
|
||||||
|
///
|
||||||
|
/// Caution: This assumes the prefix is already removed.
|
||||||
|
fn parse_value(mut input: &str) -> Result<Self, ParseDnsEntryError> {
|
||||||
|
let input = &mut input;
|
||||||
|
let enr_root = parse_value(input, "e=", "ENR Root", |s| Ok(s.to_string()))?;
|
||||||
|
let link_root = parse_value(input, "l=", "Link Root", |s| Ok(s.to_string()))?;
|
||||||
|
let sequence_number = parse_value(input, "seq=", "Sequence number", |s| {
|
||||||
|
s.parse::<u64>().map_err(|_| {
|
||||||
|
ParseDnsEntryError::Other(format!("Failed to parse sequence number {s}"))
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let signature = parse_value(input, "sig=", "Signature", |s| {
|
||||||
|
BASE64URL_NOPAD.decode(s.as_bytes()).map_err(|err| {
|
||||||
|
ParseDnsEntryError::Base64DecodeError(format!("signature error: {err}"))
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
Ok(Self { enr_root, link_root, sequence_number, signature })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the _unsigned_ content pairs of the entry:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// e=<enr-root> l=<link-root> seq=<sequence-number> sig=<signature>
|
||||||
|
/// ```
|
||||||
|
fn content(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{} e={} l={} seq={}",
|
||||||
|
ROOT_V1_PREFIX, self.enr_root, self.link_root, self.sequence_number
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the signature of the record.
|
||||||
|
#[must_use]
|
||||||
|
pub fn verify<K: EnrKey>(&self, pubkey: &K::PublicKey) -> bool {
|
||||||
|
let mut sig = self.signature.clone();
|
||||||
|
sig.truncate(64);
|
||||||
|
pubkey.verify_v4(self.content().as_bytes(), &sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TreeRootEntry {
|
||||||
|
type Err = ParseDnsEntryError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Some(s) = s.strip_prefix(ROOT_V1_PREFIX) {
|
||||||
|
Self::parse_value(s)
|
||||||
|
} else {
|
||||||
|
Err(UnknownEntry(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for TreeRootEntry {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("TreeRootEntry")
|
||||||
|
.field("enr_root", &self.enr_root)
|
||||||
|
.field("link_root", &self.link_root)
|
||||||
|
.field("sequence_number", &self.sequence_number)
|
||||||
|
.field("signature", &hex::encode(self.signature.as_ref()))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TreeRootEntry {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{} sig={}", self.content(), BASE64URL_NOPAD.encode(self.signature.as_ref()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A branch entry with base32 hashes
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BranchEntry {
|
||||||
|
children: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === impl BranchEntry ===
|
||||||
|
|
||||||
|
impl BranchEntry {
|
||||||
|
/// Parses the entry from text.
|
||||||
|
///
|
||||||
|
/// Caution: This assumes the prefix is already removed.
|
||||||
|
fn parse_value(input: &str) -> Result<Self, ParseDnsEntryError> {
|
||||||
|
let children = input.trim().split(',').map(str::to_string).collect();
|
||||||
|
Ok(Self { children })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for BranchEntry {
|
||||||
|
type Err = ParseDnsEntryError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Some(s) = s.strip_prefix(BRANCH_PREFIX) {
|
||||||
|
Self::parse_value(s)
|
||||||
|
} else {
|
||||||
|
Err(UnknownEntry(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BranchEntry {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", BRANCH_PREFIX, self.children.join(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A link entry
|
||||||
|
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||||
|
pub struct LinkEntry<K: EnrKeyUnambiguous = SecretKey> {
|
||||||
|
domain: String,
|
||||||
|
pubkey: K::PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === impl LinkEntry ===
|
||||||
|
|
||||||
|
impl<K: EnrKeyUnambiguous> LinkEntry<K> {
|
||||||
|
/// Parses the entry from text.
|
||||||
|
///
|
||||||
|
/// Caution: This assumes the prefix is already removed.
|
||||||
|
fn parse_value(input: &str) -> Result<Self, ParseDnsEntryError> {
|
||||||
|
let (pubkey, domain) = input.split_once('@').ok_or_else(|| {
|
||||||
|
ParseDnsEntryError::Other(format!("Missing @ delimiter in Link entry: {input}"))
|
||||||
|
})?;
|
||||||
|
let pubkey = K::decode_public(&BASE32_NOPAD.decode(pubkey.as_bytes()).map_err(|err| {
|
||||||
|
ParseDnsEntryError::Base32DecodeError(format!("pubkey error: {err}"))
|
||||||
|
})?)
|
||||||
|
.map_err(|err| ParseDnsEntryError::RlpDecodeError(err.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Self { domain: domain.to_string(), pubkey })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: EnrKeyUnambiguous> FromStr for LinkEntry<K> {
|
||||||
|
type Err = ParseDnsEntryError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Some(s) = s.strip_prefix(LINK_PREFIX) {
|
||||||
|
Self::parse_value(s)
|
||||||
|
} else {
|
||||||
|
Err(UnknownEntry(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: EnrKeyUnambiguous> fmt::Display for LinkEntry<K> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}{}@{}",
|
||||||
|
LINK_PREFIX,
|
||||||
|
BASE32_NOPAD.encode(self.pubkey.encode().as_ref()),
|
||||||
|
self.domain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The actual [Enr] entry.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NodeEntry<K: EnrKeyUnambiguous> {
|
||||||
|
enr: Enr<K>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === impl NodeEntry ===
|
||||||
|
|
||||||
|
impl<K: EnrKeyUnambiguous> NodeEntry<K> {
|
||||||
|
/// Parses the entry from text.
|
||||||
|
///
|
||||||
|
/// Caution: This assumes the prefix is already removed.
|
||||||
|
fn parse_value(s: &str) -> Result<Self, ParseDnsEntryError> {
|
||||||
|
let enr: Enr<K> = s.parse().map_err(ParseDnsEntryError::Other)?;
|
||||||
|
Ok(Self { enr })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: EnrKeyUnambiguous> FromStr for NodeEntry<K> {
|
||||||
|
type Err = ParseDnsEntryError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Some(s) = s.strip_prefix(ENR_PREFIX) {
|
||||||
|
Self::parse_value(s)
|
||||||
|
} else {
|
||||||
|
Err(UnknownEntry(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: EnrKeyUnambiguous> fmt::Display for NodeEntry<K> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.enr.to_base64().fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the value of the key value pair
|
||||||
|
fn parse_value<F, V>(
|
||||||
|
input: &mut &str,
|
||||||
|
key: &str,
|
||||||
|
err: &'static str,
|
||||||
|
f: F,
|
||||||
|
) -> Result<V, ParseDnsEntryError>
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> Result<V, ParseDnsEntryError>,
|
||||||
|
{
|
||||||
|
ensure_strip_key(input, key, err)?;
|
||||||
|
let val = input.split_whitespace().next().ok_or(FieldNotFound(err))?;
|
||||||
|
*input = &input[val.len()..];
|
||||||
|
|
||||||
|
f(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strips the `key` from the `input`
|
||||||
|
///
|
||||||
|
/// Returns an err if the `input` does not start with the `key`
|
||||||
|
fn ensure_strip_key(
|
||||||
|
input: &mut &str,
|
||||||
|
key: &str,
|
||||||
|
err: &'static str,
|
||||||
|
) -> Result<(), ParseDnsEntryError> {
|
||||||
|
*input = input.trim_start().strip_prefix(key).ok_or(FieldNotFound(err))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use secp256k1::SecretKey;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_root_entry() {
|
||||||
|
let s = "enrtree-root:v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE";
|
||||||
|
let root: TreeRootEntry = s.parse().unwrap();
|
||||||
|
assert_eq!(root.to_string(), s);
|
||||||
|
|
||||||
|
match s.parse::<DnsEntry<SecretKey>>().unwrap() {
|
||||||
|
DnsEntry::Root(root) => {
|
||||||
|
assert_eq!(root.to_string(), s);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_branch_entry() {
|
||||||
|
let s = "enrtree-branch:CCCCCCCCCCCCCCCCCCCC,BBBBBBBBBBBBBBBBBBBB";
|
||||||
|
let entry: BranchEntry = s.parse().unwrap();
|
||||||
|
assert_eq!(entry.to_string(), s);
|
||||||
|
|
||||||
|
match s.parse::<DnsEntry<SecretKey>>().unwrap() {
|
||||||
|
DnsEntry::Branch(entry) => {
|
||||||
|
assert_eq!(entry.to_string(), s);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_link_entry() {
|
||||||
|
let s = "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@nodes.example.org";
|
||||||
|
let entry: LinkEntry<SecretKey> = s.parse().unwrap();
|
||||||
|
assert_eq!(entry.to_string(), s);
|
||||||
|
|
||||||
|
match s.parse::<DnsEntry<SecretKey>>().unwrap() {
|
||||||
|
DnsEntry::Link(entry) => {
|
||||||
|
assert_eq!(entry.to_string(), s);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_enr_entry() {
|
||||||
|
let s = "enr:-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM";
|
||||||
|
let entry: NodeEntry<SecretKey> = s.parse().unwrap();
|
||||||
|
assert_eq!(entry.to_string(), s);
|
||||||
|
|
||||||
|
match s.parse::<DnsEntry<SecretKey>>().unwrap() {
|
||||||
|
DnsEntry::Node(entry) => {
|
||||||
|
assert_eq!(entry.to_string(), s);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user