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:
Matthias Seitz
2023-01-09 18:09:09 +01:00
committed by GitHub
parent d522abb749
commit 2b3dfe93a7
8 changed files with 735 additions and 5 deletions

138
Cargo.lock generated
View File

@ -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"

View File

@ -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
View 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"

View 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
View 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 {}

View 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()
}
}

View 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
View 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 trees 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!(),
}
}
}