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]]
|
||||
name = "async-trait"
|
||||
version = "0.1.60"
|
||||
version = "0.1.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3"
|
||||
checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1277,6 +1277,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "enum-ordinalize"
|
||||
version = "3.1.12"
|
||||
@ -1912,6 +1924,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "http"
|
||||
version = "0.2.8"
|
||||
@ -2167,6 +2190,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ipnet"
|
||||
version = "2.7.0"
|
||||
@ -2500,6 +2535,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "mach"
|
||||
version = "0.3.2"
|
||||
@ -2509,6 +2553,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@ -3211,7 +3261,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
"trust-dns-client",
|
||||
"trust-dns-proto",
|
||||
"trust-dns-proto 0.20.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3467,6 +3517,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "reth"
|
||||
version = "0.1.0"
|
||||
@ -3595,6 +3655,23 @@ dependencies = [
|
||||
"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]]
|
||||
name = "reth-downloaders"
|
||||
version = "0.1.0"
|
||||
@ -5298,7 +5375,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"trust-dns-proto",
|
||||
"trust-dns-proto 0.20.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5310,7 +5387,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"enum-as-inner 0.3.4",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
@ -5326,6 +5403,51 @@ dependencies = [
|
||||
"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]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
@ -5618,6 +5740,12 @@ dependencies = [
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
|
||||
|
||||
[[package]]
|
||||
name = "wildmatch"
|
||||
version = "1.1.0"
|
||||
|
||||
@ -11,6 +11,7 @@ members = [
|
||||
"crates/net/ecies",
|
||||
"crates/net/eth-wire",
|
||||
"crates/net/discv4",
|
||||
"crates/net/dns",
|
||||
"crates/net/nat",
|
||||
"crates/net/network",
|
||||
"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