feat: resolve domains in enode strings (#8188)

Co-authored-by: Serge Radinovich <47865535+sergerad@users.noreply.github.com>
This commit is contained in:
Dan Cline
2024-06-05 19:43:25 -04:00
committed by GitHub
parent c5e38073b5
commit ef3f67743d
22 changed files with 470 additions and 64 deletions

72
Cargo.lock generated
View File

@ -140,7 +140,7 @@ dependencies = [
[[package]]
name = "alloy-consensus"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy#4ecb7d86882ece8a9a7a5a892b71a3c198030731"
source = "git+https://github.com/alloy-rs/alloy#eaf53556d1ee4ac0e611d6c06e3732a24f1d11ab"
dependencies = [
"alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy)",
"alloy-primitives",
@ -165,7 +165,7 @@ dependencies = [
"itoa",
"serde",
"serde_json",
"winnow 0.6.9",
"winnow 0.6.10",
]
[[package]]
@ -189,7 +189,7 @@ dependencies = [
[[package]]
name = "alloy-eips"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy#4ecb7d86882ece8a9a7a5a892b71a3c198030731"
source = "git+https://github.com/alloy-rs/alloy#eaf53556d1ee4ac0e611d6c06e3732a24f1d11ab"
dependencies = [
"alloy-primitives",
"alloy-rlp",
@ -214,7 +214,7 @@ dependencies = [
[[package]]
name = "alloy-genesis"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy#4ecb7d86882ece8a9a7a5a892b71a3c198030731"
source = "git+https://github.com/alloy-rs/alloy#eaf53556d1ee4ac0e611d6c06e3732a24f1d11ab"
dependencies = [
"alloy-primitives",
"alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy)",
@ -405,7 +405,7 @@ dependencies = [
[[package]]
name = "alloy-rpc-types"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy#4ecb7d86882ece8a9a7a5a892b71a3c198030731"
source = "git+https://github.com/alloy-rs/alloy#eaf53556d1ee4ac0e611d6c06e3732a24f1d11ab"
dependencies = [
"alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy)",
"alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy)",
@ -486,7 +486,7 @@ dependencies = [
[[package]]
name = "alloy-serde"
version = "0.1.0"
source = "git+https://github.com/alloy-rs/alloy#4ecb7d86882ece8a9a7a5a892b71a3c198030731"
source = "git+https://github.com/alloy-rs/alloy#eaf53556d1ee4ac0e611d6c06e3732a24f1d11ab"
dependencies = [
"alloy-primitives",
"serde",
@ -579,7 +579,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368cae4dc052cad1d8f72eb2ae0c38027116933eeb49213c200a9e9875f208d7"
dependencies = [
"winnow 0.6.9",
"winnow 0.6.10",
]
[[package]]
@ -2989,7 +2989,7 @@ dependencies = [
[[package]]
name = "foundry-blob-explorers"
version = "0.1.0"
source = "git+https://github.com/foundry-rs/block-explorers#1b024125d8327595f67f18a60ac29c49056c3a6d"
source = "git+https://github.com/foundry-rs/block-explorers#1674a68b073a3637c16f2d3f9700cf6332ffe4a6"
dependencies = [
"alloy-chains",
"alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy)",
@ -3554,9 +3554,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "0.14.28"
version = "0.14.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
dependencies = [
"bytes",
"futures-channel",
@ -3603,7 +3603,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.28",
"hyper 0.14.29",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
@ -4154,7 +4154,7 @@ dependencies = [
"beef",
"futures-timer",
"futures-util",
"hyper 0.14.28",
"hyper 0.14.29",
"jsonrpsee-types",
"parking_lot 0.12.3",
"pin-project",
@ -4176,7 +4176,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ccf93fc4a0bfe05d851d37d7c32b7f370fe94336b52a2f0efc5f1981895c2e5"
dependencies = [
"async-trait",
"hyper 0.14.28",
"hyper 0.14.29",
"hyper-rustls 0.24.2",
"jsonrpsee-core",
"jsonrpsee-types",
@ -4210,7 +4210,7 @@ checksum = "12d8b6a9674422a8572e0b0abb12feeb3f2aeda86528c80d0350c2bd0923ab41"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.28",
"hyper 0.14.29",
"jsonrpsee-core",
"jsonrpsee-types",
"pin-project",
@ -5716,9 +5716,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.84"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [
"unicode-ident",
]
@ -6070,7 +6070,7 @@ dependencies = [
"h2",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper 0.14.29",
"ipnet",
"js-sys",
"log",
@ -6191,6 +6191,7 @@ dependencies = [
"reth-evm",
"reth-exex",
"reth-fs-util",
"reth-net-common",
"reth-network",
"reth-network-api",
"reth-network-p2p",
@ -7117,6 +7118,7 @@ dependencies = [
"serde_json",
"serde_with",
"thiserror",
"tokio",
"url",
]
@ -7160,6 +7162,7 @@ name = "reth-node-builder"
version = "0.2.0-beta.8"
dependencies = [
"aquamarine",
"backon",
"confy",
"eyre",
"fdlimit",
@ -7212,7 +7215,7 @@ dependencies = [
"eyre",
"futures",
"humantime",
"hyper 0.14.28",
"hyper 0.14.29",
"metrics",
"metrics-exporter-prometheus",
"metrics-process",
@ -7235,6 +7238,7 @@ dependencies = [
"reth-net-nat",
"reth-network",
"reth-network-p2p",
"reth-network-types",
"reth-primitives",
"reth-provider",
"reth-rpc",
@ -7316,7 +7320,7 @@ dependencies = [
"async-trait",
"clap",
"eyre",
"hyper 0.14.28",
"hyper 0.14.29",
"jsonrpsee",
"parking_lot 0.12.3",
"reqwest 0.12.4",
@ -7557,7 +7561,7 @@ dependencies = [
"futures",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper 0.14.29",
"jsonrpsee",
"jsonwebtoken 8.3.0",
"metrics",
@ -7630,7 +7634,7 @@ dependencies = [
name = "reth-rpc-builder"
version = "0.2.0-beta.8"
dependencies = [
"hyper 0.14.28",
"hyper 0.14.29",
"jsonrpsee",
"metrics",
"pin-project",
@ -7705,7 +7709,7 @@ dependencies = [
"assert_matches",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper 0.14.29",
"jsonrpsee",
"pin-project",
"tempfile",
@ -8210,9 +8214,9 @@ dependencies = [
[[package]]
name = "ruint"
version = "1.12.1"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f308135fef9fc398342da5472ce7c484529df23743fb7c734e0f3d472971e62"
checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286"
dependencies = [
"alloy-rlp",
"arbitrary",
@ -8235,9 +8239,9 @@ dependencies = [
[[package]]
name = "ruint-macro"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86854cf50259291520509879a5c294c3c9a4c334e9ff65071c51e42ef1e2343"
checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18"
[[package]]
name = "rusqlite"
@ -9530,14 +9534,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.13"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.13",
"toml_edit 0.22.14",
]
[[package]]
@ -9562,15 +9566,15 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.13"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [
"indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.9",
"winnow 0.6.10",
]
[[package]]
@ -10451,9 +10455,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.6.9"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6"
checksum = "f217b6745021054125ef5741032a021a9c65f82bee2a8017cca928f1e3179991"
dependencies = [
"memchr",
]

View File

@ -378,6 +378,7 @@ dyn-clone = "1.0.17"
sha2 = { version = "0.10", default-features = false }
paste = "1.0"
url = "2.3"
backon = "0.4"
# metrics
metrics = "0.22.0"

View File

@ -37,6 +37,7 @@ reth-rpc-types-compat.workspace = true
reth-rpc-api = { workspace = true, features = ["client"] }
reth-network = { workspace = true, features = ["serde"] }
reth-network-p2p.workspace = true
reth-net-common.workspace = true
reth-network-api.workspace = true
reth-downloaders.workspace = true
reth-tracing.workspace = true
@ -104,7 +105,7 @@ aquamarine.workspace = true
eyre.workspace = true
clap = { workspace = true, features = ["derive", "env"] }
tempfile.workspace = true
backon = "0.4"
backon.workspace = true
similar-asserts.workspace = true
itertools.workspace = true
rayon.workspace = true

View File

@ -88,8 +88,8 @@ impl Command {
let mut config: Config = confy::load_path(&config_path).unwrap_or_default();
for &peer in &self.network.trusted_peers {
config.peers.trusted_nodes.insert(peer);
for peer in &self.network.trusted_peers {
config.peers.trusted_nodes.insert(peer.resolve().await?);
}
if config.peers.trusted_nodes.is_empty() && self.network.trusted_only {

View File

@ -118,9 +118,10 @@ impl Command {
let mut config = config;
config.peers.trusted_nodes_only = self.network.trusted_only;
if !self.network.trusted_peers.is_empty() {
self.network.trusted_peers.iter().for_each(|peer| {
config.peers.trusted_nodes.insert(*peer);
});
for peer in &self.network.trusted_peers {
let peer = peer.resolve().await?;
config.peers.trusted_nodes.insert(peer);
}
}
let network_secret_path = self

View File

@ -126,6 +126,11 @@ Networking:
Will fall back to a network-specific default if not specified.
--dns-retries <DNS_RETRIES>
Amount of DNS resolution requests retries to perform when peering
[default: 0]
--peers-file <FILE>
The path to the known peers file. Connected peers are dumped to this file on nodes
shutdown, and read on startup. Cannot be used with `--no-persist-peers`.

View File

@ -110,6 +110,11 @@ Networking:
Will fall back to a network-specific default if not specified.
--dns-retries <DNS_RETRIES>
Amount of DNS resolution requests retries to perform when peering
[default: 0]
--peers-file <FILE>
The path to the known peers file. Connected peers are dumped to this file on nodes
shutdown, and read on startup. Cannot be used with `--no-persist-peers`.

View File

@ -177,6 +177,11 @@ Networking:
Will fall back to a network-specific default if not specified.
--dns-retries <DNS_RETRIES>
Amount of DNS resolution requests retries to perform when peering
[default: 0]
--peers-file <FILE>
The path to the known peers file. Connected peers are dumped to this file on nodes
shutdown, and read on startup. Cannot be used with `--no-persist-peers`.

View File

@ -139,6 +139,11 @@ Networking:
Will fall back to a network-specific default if not specified.
--dns-retries <DNS_RETRIES>
Amount of DNS resolution requests retries to perform when peering
[default: 0]
--peers-file <FILE>
The path to the known peers file. Connected peers are dumped to this file on nodes
shutdown, and read on startup. Cannot be used with `--no-persist-peers`.

View File

@ -10,6 +10,7 @@
pub mod ban_list;
pub mod bandwidth_meter;
/// Traits related to tokio streams
pub mod stream;

View File

@ -14,7 +14,7 @@ use reth_dns_discovery::DnsDiscoveryConfig;
use reth_eth_wire::{HelloMessage, HelloMessageWithProtocols, Status};
use reth_network_types::{pk2id, PeerId};
use reth_primitives::{
mainnet_nodes, sepolia_nodes, ChainSpec, ForkFilter, Head, NodeRecord, MAINNET,
mainnet_nodes, sepolia_nodes, ChainSpec, ForkFilter, Head, TrustedPeer, MAINNET,
};
use reth_provider::{BlockReader, HeaderProvider};
use reth_tasks::{TaskSpawner, TokioTaskExecutor};
@ -41,7 +41,7 @@ pub struct NetworkConfig<C> {
/// The node's secret key, from which the node's identity is derived.
pub secret_key: SecretKey,
/// All boot nodes to start network discovery with.
pub boot_nodes: HashSet<NodeRecord>,
pub boot_nodes: HashSet<TrustedPeer>,
/// How to set up discovery over DNS.
pub dns_discovery_config: Option<DnsDiscoveryConfig>,
/// Address to use for discovery v4.
@ -147,7 +147,7 @@ pub struct NetworkConfigBuilder {
#[serde(skip)]
discovery_v5_builder: Option<reth_discv5::ConfigBuilder>,
/// All boot nodes to start network discovery with.
boot_nodes: HashSet<NodeRecord>,
boot_nodes: HashSet<TrustedPeer>,
/// Address to use for discovery
discovery_addr: Option<SocketAddr>,
/// Listener for incoming connections
@ -365,8 +365,8 @@ impl NetworkConfigBuilder {
}
/// Sets the boot nodes.
pub fn boot_nodes(mut self, nodes: impl IntoIterator<Item = NodeRecord>) -> Self {
self.boot_nodes = nodes.into_iter().collect();
pub fn boot_nodes<T: Into<TrustedPeer>>(mut self, nodes: impl IntoIterator<Item = T>) -> Self {
self.boot_nodes = nodes.into_iter().map(Into::into).collect();
self
}

View File

@ -206,9 +206,16 @@ where
})?;
let listener_address = Arc::new(Mutex::new(incoming.local_address()));
// resolve boot nodes
let mut resolved_boot_nodes = vec![];
for record in &boot_nodes {
let resolved = record.resolve().await?;
resolved_boot_nodes.push(resolved);
}
discovery_v4_config = discovery_v4_config.map(|mut disc_config| {
// merge configured boot nodes
disc_config.bootstrap_nodes.extend(boot_nodes.clone());
disc_config.bootstrap_nodes.extend(resolved_boot_nodes.clone());
disc_config.add_eip868_pair("eth", status.forkid);
disc_config
});

View File

@ -25,6 +25,7 @@ secp256k1.workspace = true
serde_with.workspace = true
thiserror.workspace = true
url.workspace = true
tokio = { workspace = true, features = ["full"] }
[dev-dependencies]
alloy-primitives = { workspace = true, features = ["rand"] }

View File

@ -2,6 +2,43 @@
//!
//! This crate manages and converts Ethereum network entities such as node records, peer IDs, and
//! Ethereum Node Records (ENRs)
//!
//! ## An overview of Node Record types
//!
//! Ethereum uses different types of "node records" to represent peers on the network.
//!
//! The simplest way to identify a peer is by public key. This is the [`PeerId`] type, which usually
//! represents a peer's secp256k1 public key.
//!
//! A more complete representation of a peer is the [`NodeRecord`] type, which includes the peer's
//! IP address, the ports where it is reachable (TCP and UDP), and the peer's public key. This is
//! what is returned from discovery v4 queries.
//!
//! The most comprehensive node record type is the Ethereum Node Record ([`Enr`]), which is a
//! signed, versioned record that includes the information from a [`NodeRecord`] along with
//! additional metadata. This is the data structure returned from discovery v5 queries.
//!
//! When we need to deserialize an identifier that could be any of these three types ([`PeerId`],
//! [`NodeRecord`], and [`Enr`]), we use the [`AnyNode`] type, which is an enum over the three
//! types. [`AnyNode`] is used in reth's `admin_addTrustedPeer` RPC method.
//!
//! The __final__ type is the [`TrustedPeer`] type, which is similar to a [`NodeRecord`] but may
//! include a domain name instead of a direct IP address. It includes a `resolve` method, which can
//! be used to resolve the domain name, producing a [`NodeRecord`]. This is useful for adding
//! trusted peers at startup, whose IP address may not be static each time the node starts. This is
//! common in orchestrated environments like Kubernetes, where there is reliable service discovery,
//! but services do not necessarily have static IPs.
//!
//! In short, the types are as follows:
//! - [`PeerId`]: A simple public key identifier.
//! - [`NodeRecord`]: A more complete representation of a peer, including IP address and ports.
//! - [`Enr`]: An Ethereum Node Record, which is a signed, versioned record that includes additional
//! metadata. Useful when interacting with discovery v5, or when custom metadata is required.
//! - [`AnyNode`]: An enum over [`PeerId`], [`NodeRecord`], and [`Enr`], useful in deserialization
//! when the type of the node record is not known.
//! - [`TrustedPeer`]: A [`NodeRecord`] with an optional domain name, which can be resolved to a
//! [`NodeRecord`]. Useful for adding trusted peers at startup, whose IP address may not be
//! static.
#![doc(
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
@ -24,6 +61,9 @@ pub type PeerId = B512;
pub mod node_record;
pub use node_record::{NodeRecord, NodeRecordParseError};
pub mod trusted_peer;
pub use trusted_peer::TrustedPeer;
/// This tag should be set to indicate to libsecp256k1 that the following bytes denote an
/// uncompressed pubkey.
///

View File

@ -0,0 +1,300 @@
//! `NodeRecord` type that uses a domain instead of an IP.
use std::{
fmt::{self, Write},
io::Error,
net::IpAddr,
num::ParseIntError,
str::FromStr,
};
use crate::{NodeRecord, PeerId};
use secp256k1::{SecretKey, SECP256K1};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use url::Host;
/// Represents the node record of a trusted peer. The only difference between this and a
/// [`NodeRecord`] is that this does not contain the IP address of the peer, but rather a domain
/// __or__ IP address.
///
/// This is useful when specifying nodes which are in internal infrastructure and may only be
/// discoverable reliably using DNS.
///
/// This should NOT be used for any use case other than in trusted peer lists.
#[derive(Clone, Debug, Eq, PartialEq, Hash, SerializeDisplay, DeserializeFromStr)]
pub struct TrustedPeer {
/// The host of a node.
pub host: Host,
/// TCP port of the port that accepts connections.
pub tcp_port: u16,
/// UDP discovery port.
pub udp_port: u16,
/// Public key of the discovery service
pub id: PeerId,
}
impl TrustedPeer {
/// Derive the [`NodeRecord`] from the secret key and addr
pub fn from_secret_key(host: Host, port: u16, sk: &SecretKey) -> Self {
let pk = secp256k1::PublicKey::from_secret_key(SECP256K1, sk);
let id = PeerId::from_slice(&pk.serialize_uncompressed()[1..]);
Self::new(host, port, id)
}
/// Creates a new record from a socket addr and peer id.
pub const fn new(host: Host, port: u16, id: PeerId) -> Self {
Self { host, tcp_port: port, udp_port: port, id }
}
/// Resolves the host in a [`TrustedPeer`] to an IP address, returning a [`NodeRecord`].
pub async fn resolve(&self) -> Result<NodeRecord, Error> {
let domain = match self.host.to_owned() {
Host::Ipv4(ip) => {
let id = self.id;
let tcp_port = self.tcp_port;
let udp_port = self.udp_port;
return Ok(NodeRecord { address: ip.into(), id, tcp_port, udp_port })
}
Host::Ipv6(ip) => {
let id = self.id;
let tcp_port = self.tcp_port;
let udp_port = self.udp_port;
return Ok(NodeRecord { address: ip.into(), id, tcp_port, udp_port })
}
Host::Domain(domain) => domain,
};
// Resolve the domain to an IP address
let mut ips = tokio::net::lookup_host(format!("{domain}:0")).await?;
let ip = ips
.next()
.ok_or_else(|| Error::new(std::io::ErrorKind::AddrNotAvailable, "No IP found"))?;
Ok(NodeRecord {
address: ip.ip(),
id: self.id,
tcp_port: self.tcp_port,
udp_port: self.udp_port,
})
}
}
impl fmt::Display for TrustedPeer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("enode://")?;
alloy_primitives::hex::encode(self.id.as_slice()).fmt(f)?;
f.write_char('@')?;
self.host.fmt(f)?;
f.write_char(':')?;
self.tcp_port.fmt(f)?;
if self.tcp_port != self.udp_port {
f.write_str("?discport=")?;
self.udp_port.fmt(f)?;
}
Ok(())
}
}
/// Possible error types when parsing a [`NodeRecord`]
#[derive(Debug, thiserror::Error)]
pub enum NodeRecordParseError {
/// Invalid url
#[error("Failed to parse url: {0}")]
InvalidUrl(String),
/// Invalid id
#[error("Failed to parse id")]
InvalidId(String),
/// Invalid discport
#[error("Failed to discport query: {0}")]
Discport(ParseIntError),
}
impl FromStr for TrustedPeer {
type Err = NodeRecordParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use url::Url;
// Parse the URL with enode prefix replaced with http.
// The enode prefix causes the parser to use parse_opaque() on
// the host str which only handles domains and ipv6, not ipv4.
let url = Url::parse(s.replace("enode://", "http://").as_str())
.map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?;
let host = url
.host()
.ok_or_else(|| NodeRecordParseError::InvalidUrl("no host specified".to_string()))?
.to_owned();
let port = url
.port()
.ok_or_else(|| NodeRecordParseError::InvalidUrl("no port specified".to_string()))?;
let udp_port = if let Some(discovery_port) = url
.query_pairs()
.find_map(|(maybe_disc, port)| (maybe_disc.as_ref() == "discport").then_some(port))
{
discovery_port.parse::<u16>().map_err(NodeRecordParseError::Discport)?
} else {
port
};
let id = url
.username()
.parse::<PeerId>()
.map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
Ok(Self { host, id, tcp_port: port, udp_port })
}
}
impl From<NodeRecord> for TrustedPeer {
fn from(record: NodeRecord) -> Self {
let host = match record.address {
IpAddr::V4(ip) => Host::Ipv4(ip),
IpAddr::V6(ip) => Host::Ipv6(ip),
};
Self { host, tcp_port: record.tcp_port, udp_port: record.udp_port, id: record.id }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv6Addr;
#[test]
fn test_url_parse() {
let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
let node: TrustedPeer = url.parse().unwrap();
assert_eq!(node, TrustedPeer {
host: Host::Ipv4([10,3,58,6].into()),
tcp_port: 30303,
udp_port: 30301,
id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(),
})
}
#[test]
fn test_node_display() {
let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303";
let node: TrustedPeer = url.parse().unwrap();
assert_eq!(url, &format!("{node}"));
}
#[test]
fn test_node_display_discport() {
let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
let node: TrustedPeer = url.parse().unwrap();
assert_eq!(url, &format!("{node}"));
}
#[test]
fn test_node_serialize() {
let cases = vec![
// IPv4
(
TrustedPeer {
host: Host::Ipv4([10, 3, 58, 6].into()),
tcp_port: 30303u16,
udp_port: 30301u16,
id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
},
"\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\""
),
// IPv6
(
TrustedPeer {
host: Host::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12)),
tcp_port: 52150u16,
udp_port: 52151u16,
id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
},
"\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\""
),
// URL
(
TrustedPeer {
host: Host::Domain("my-domain".to_string()),
tcp_port: 52150u16,
udp_port: 52151u16,
id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
},
"\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@my-domain:52150?discport=52151\""
),
];
for (node, expected) in cases {
let ser = serde_json::to_string::<TrustedPeer>(&node).expect("couldn't serialize");
assert_eq!(ser, expected);
}
}
#[test]
fn test_node_deserialize() {
let cases = vec![
// IPv4
(
"\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"",
TrustedPeer {
host: Host::Ipv4([10, 3, 58, 6].into()),
tcp_port: 30303u16,
udp_port: 30301u16,
id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
}
),
// IPv6
(
"\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
TrustedPeer {
host: Host::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12)),
tcp_port: 52150u16,
udp_port: 52151u16,
id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
}
),
// URL
(
"\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@my-domain:52150?discport=52151\"",
TrustedPeer {
host: Host::Domain("my-domain".to_string()),
tcp_port: 52150u16,
udp_port: 52151u16,
id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
}
),
];
for (url, expected) in cases {
let node: TrustedPeer = serde_json::from_str(url).expect("couldn't deserialize");
assert_eq!(node, expected);
}
}
#[tokio::test]
async fn test_resolve_dns_node_record() {
// Set up tests
let tests = vec![("localhost")];
// Run tests
for domain in tests {
// Construct record
let rec =
TrustedPeer::new(url::Host::Domain(domain.to_owned()), 30300, PeerId::random());
// Resolve domain and validate
let rec = rec.resolve().await.unwrap();
match rec.address {
std::net::IpAddr::V4(addr) => {
assert_eq!(addr, std::net::Ipv4Addr::new(127, 0, 0, 1))
}
std::net::IpAddr::V6(addr) => {
assert_eq!(addr, std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
}
}
}
}
}

View File

@ -96,6 +96,7 @@ procfs = "0.16.0"
[dev-dependencies]
# test vectors generation
proptest.workspace = true
reth-network-types.workspace = true
[features]
optimism = [

View File

@ -17,7 +17,7 @@ use reth_network::{
},
HelloMessageWithProtocols, NetworkConfigBuilder, SessionsConfig,
};
use reth_primitives::{mainnet_nodes, ChainSpec, NodeRecord};
use reth_primitives::{mainnet_nodes, ChainSpec, TrustedPeer};
use secp256k1::SecretKey;
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
@ -39,7 +39,7 @@ pub struct NetworkArgs {
///
/// --trusted-peers enode://abcd@192.168.0.1:30303
#[arg(long, value_delimiter = ',')]
pub trusted_peers: Vec<NodeRecord>,
pub trusted_peers: Vec<TrustedPeer>,
/// Connect to or accept from trusted peers only
#[arg(long)]
@ -49,7 +49,11 @@ pub struct NetworkArgs {
///
/// Will fall back to a network-specific default if not specified.
#[arg(long, value_delimiter = ',')]
pub bootnodes: Option<Vec<NodeRecord>>,
pub bootnodes: Option<Vec<TrustedPeer>>,
/// Amount of DNS resolution requests retries to perform when peering.
#[arg(long, default_value_t = 0)]
pub dns_retries: usize,
/// The path to the known peers file. Connected peers are dumped to this file on nodes
/// shutdown, and read on startup. Cannot be used with `--no-persist-peers`.
@ -125,10 +129,7 @@ impl NetworkArgs {
secret_key: SecretKey,
default_peers_file: PathBuf,
) -> NetworkConfigBuilder {
let boot_nodes = self
.bootnodes
.clone()
.unwrap_or_else(|| chain_spec.bootnodes().unwrap_or_else(mainnet_nodes));
let chain_bootnodes = chain_spec.bootnodes().unwrap_or_else(mainnet_nodes);
let peers_file = self.peers_file.clone().unwrap_or(default_peers_file);
// Configure peer connections
@ -156,7 +157,7 @@ impl NetworkArgs {
SessionsConfig::default().with_upscaled_event_buffer(peers_config.max_peers()),
)
.peer_config(peers_config)
.boot_nodes(boot_nodes.clone())
.boot_nodes(chain_bootnodes.clone())
.chain_spec(chain_spec.clone())
.transactions_manager_config(transactions_manager_config)
// Configure node identity
@ -189,7 +190,7 @@ impl NetworkArgs {
} = self.discovery;
builder
.add_unsigned_boot_nodes(boot_nodes.into_iter())
.add_unsigned_boot_nodes(chain_bootnodes.into_iter())
.lookup_interval(discv5_lookup_interval)
.bootstrap_lookup_interval(discv5_bootstrap_lookup_interval)
.bootstrap_lookup_countdown(discv5_bootstrap_lookup_countdown)
@ -224,6 +225,7 @@ impl Default for NetworkArgs {
trusted_peers: vec![],
trusted_only: false,
bootnodes: None,
dns_retries: 0,
peers_file: None,
identity: P2P_CLIENT_VERSION.to_string(),
p2p_secret_key: None,
@ -416,6 +418,22 @@ mod tests {
);
}
#[test]
fn parse_retry_strategy_args() {
let tests = vec![0, 10];
for retries in tests {
let args = CommandParser::<NetworkArgs>::parse_from([
"reth",
"--dns-retries",
retries.to_string().as_str(),
])
.args;
assert_eq!(args.dns_retries, retries);
}
}
#[cfg(not(feature = "optimism"))]
#[test]
fn network_args_default_sanity_test() {

View File

@ -58,6 +58,7 @@ eyre.workspace = true
fdlimit.workspace = true
confy.workspace = true
rayon.workspace = true
backon.workspace = true
[dev-dependencies]
tempfile.workspace = true

View File

@ -1,5 +1,6 @@
//! Helper types that can be used by launchers.
use backon::{ConstantBuilder, Retryable};
use eyre::Context;
use rayon::ThreadPoolBuilder;
use reth_auto_seal_consensus::MiningMode;
@ -56,17 +57,19 @@ impl LaunchContext {
/// `config`.
///
/// Attaches both the `NodeConfig` and the loaded `reth.toml` config to the launch context.
pub fn with_loaded_toml_config(
pub async fn with_loaded_toml_config(
self,
config: NodeConfig,
) -> eyre::Result<LaunchContextWith<WithConfigs>> {
let toml_config = self.load_toml_config(&config)?;
let toml_config = self.load_toml_config(&config).await?;
Ok(self.with(WithConfigs { config, toml_config }))
}
/// Loads the reth config with the configured `data_dir` and overrides settings according to the
/// `config`.
pub fn load_toml_config(&self, config: &NodeConfig) -> eyre::Result<reth_config::Config> {
///
/// This is async because the trusted peers may have to be resolved.
pub async fn load_toml_config(&self, config: &NodeConfig) -> eyre::Result<reth_config::Config> {
let config_path = config.config.clone().unwrap_or_else(|| self.data_dir.config());
let mut toml_config = confy::load_path::<reth_config::Config>(&config_path)
@ -81,9 +84,16 @@ impl LaunchContext {
if !config.network.trusted_peers.is_empty() {
info!(target: "reth::cli", "Adding trusted nodes");
config.network.trusted_peers.iter().for_each(|peer| {
toml_config.peers.trusted_nodes.insert(*peer);
});
// resolve trusted peers if they use a domain instead of dns
for peer in &config.network.trusted_peers {
let backoff = ConstantBuilder::default().with_max_times(config.network.dns_retries);
let resolved = (move || { peer.resolve() })
.retry(&backoff)
.notify(|err, _| warn!(target: "reth::cli", "Error resolving peer domain: {err}. Retrying..."))
.await?;
toml_config.peers.trusted_nodes.insert(resolved);
}
}
Ok(toml_config)

View File

@ -94,7 +94,7 @@ where
let ctx = ctx
.with_configured_globals()
// load the toml config
.with_loaded_toml_config(config)?
.with_loaded_toml_config(config).await?
// attach the database
.attach(database.clone())
// ensure certain settings take effect

View File

@ -73,7 +73,7 @@ pub use integer_list::IntegerList;
pub use log::{logs_bloom, Log};
pub use net::{
goerli_nodes, holesky_nodes, mainnet_nodes, parse_nodes, sepolia_nodes, NodeRecord,
NodeRecordParseError, GOERLI_BOOTNODES, HOLESKY_BOOTNODES, MAINNET_BOOTNODES,
NodeRecordParseError, TrustedPeer, GOERLI_BOOTNODES, HOLESKY_BOOTNODES, MAINNET_BOOTNODES,
SEPOLIA_BOOTNODES,
};
pub use prune::{

View File

@ -1,4 +1,4 @@
pub use reth_network_types::{NodeRecord, NodeRecordParseError};
pub use reth_network_types::{NodeRecord, NodeRecordParseError, TrustedPeer};
// Ethereum bootnodes come from <https://github.com/ledgerwatch/erigon/blob/devel/params/bootnodes.go>
// OP bootnodes come from <https://github.com/ethereum-optimism/op-geth/blob/optimism/params/bootnodes.go>