diff --git a/Cargo.lock b/Cargo.lock index 7b1169570..bddd50409 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 3547dada7..f14f8fc29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/bin/reth/Cargo.toml b/bin/reth/Cargo.toml index 8603e350d..0c1560d82 100644 --- a/bin/reth/Cargo.toml +++ b/bin/reth/Cargo.toml @@ -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 diff --git a/bin/reth/src/commands/p2p/mod.rs b/bin/reth/src/commands/p2p/mod.rs index a85891af7..b57a2f07a 100644 --- a/bin/reth/src/commands/p2p/mod.rs +++ b/bin/reth/src/commands/p2p/mod.rs @@ -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 { diff --git a/bin/reth/src/commands/stage/run.rs b/bin/reth/src/commands/stage/run.rs index b741cda83..01a57fd52 100644 --- a/bin/reth/src/commands/stage/run.rs +++ b/bin/reth/src/commands/stage/run.rs @@ -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 diff --git a/book/cli/reth/node.md b/book/cli/reth/node.md index 29b1738d0..65fcd824d 100644 --- a/book/cli/reth/node.md +++ b/book/cli/reth/node.md @@ -126,6 +126,11 @@ Networking: Will fall back to a network-specific default if not specified. + --dns-retries + Amount of DNS resolution requests retries to perform when peering + + [default: 0] + --peers-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`. diff --git a/book/cli/reth/p2p.md b/book/cli/reth/p2p.md index d10f09f8d..ada874d8b 100644 --- a/book/cli/reth/p2p.md +++ b/book/cli/reth/p2p.md @@ -110,6 +110,11 @@ Networking: Will fall back to a network-specific default if not specified. + --dns-retries + Amount of DNS resolution requests retries to perform when peering + + [default: 0] + --peers-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`. diff --git a/book/cli/reth/stage/run.md b/book/cli/reth/stage/run.md index 4225a971d..a98a2be6d 100644 --- a/book/cli/reth/stage/run.md +++ b/book/cli/reth/stage/run.md @@ -177,6 +177,11 @@ Networking: Will fall back to a network-specific default if not specified. + --dns-retries + Amount of DNS resolution requests retries to perform when peering + + [default: 0] + --peers-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`. diff --git a/book/cli/reth/stage/unwind.md b/book/cli/reth/stage/unwind.md index 16f26cab5..a1a538f3b 100644 --- a/book/cli/reth/stage/unwind.md +++ b/book/cli/reth/stage/unwind.md @@ -139,6 +139,11 @@ Networking: Will fall back to a network-specific default if not specified. + --dns-retries + Amount of DNS resolution requests retries to perform when peering + + [default: 0] + --peers-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`. diff --git a/crates/net/common/src/lib.rs b/crates/net/common/src/lib.rs index f9eed8344..73b6b89b5 100644 --- a/crates/net/common/src/lib.rs +++ b/crates/net/common/src/lib.rs @@ -10,6 +10,7 @@ pub mod ban_list; pub mod bandwidth_meter; + /// Traits related to tokio streams pub mod stream; diff --git a/crates/net/network/src/config.rs b/crates/net/network/src/config.rs index a4682d6fa..26fd3f8a6 100644 --- a/crates/net/network/src/config.rs +++ b/crates/net/network/src/config.rs @@ -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 { /// 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, + pub boot_nodes: HashSet, /// How to set up discovery over DNS. pub dns_discovery_config: Option, /// Address to use for discovery v4. @@ -147,7 +147,7 @@ pub struct NetworkConfigBuilder { #[serde(skip)] discovery_v5_builder: Option, /// All boot nodes to start network discovery with. - boot_nodes: HashSet, + boot_nodes: HashSet, /// Address to use for discovery discovery_addr: Option, /// Listener for incoming connections @@ -365,8 +365,8 @@ impl NetworkConfigBuilder { } /// Sets the boot nodes. - pub fn boot_nodes(mut self, nodes: impl IntoIterator) -> Self { - self.boot_nodes = nodes.into_iter().collect(); + pub fn boot_nodes>(mut self, nodes: impl IntoIterator) -> Self { + self.boot_nodes = nodes.into_iter().map(Into::into).collect(); self } diff --git a/crates/net/network/src/manager.rs b/crates/net/network/src/manager.rs index c16082e1b..318b48729 100644 --- a/crates/net/network/src/manager.rs +++ b/crates/net/network/src/manager.rs @@ -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 }); diff --git a/crates/net/types/Cargo.toml b/crates/net/types/Cargo.toml index 9be9a2f3a..c406f13d3 100644 --- a/crates/net/types/Cargo.toml +++ b/crates/net/types/Cargo.toml @@ -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"] } diff --git a/crates/net/types/src/lib.rs b/crates/net/types/src/lib.rs index 327fd0c9c..40e8b638a 100644 --- a/crates/net/types/src/lib.rs +++ b/crates/net/types/src/lib.rs @@ -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. /// diff --git a/crates/net/types/src/trusted_peer.rs b/crates/net/types/src/trusted_peer.rs new file mode 100644 index 000000000..7a973f056 --- /dev/null +++ b/crates/net/types/src/trusted_peer.rs @@ -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 { + 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 { + 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::().map_err(NodeRecordParseError::Discport)? + } else { + port + }; + + let id = url + .username() + .parse::() + .map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?; + + Ok(Self { host, id, tcp_port: port, udp_port }) + } +} + +impl From 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::(&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)) + } + } + } + } +} diff --git a/crates/node-core/Cargo.toml b/crates/node-core/Cargo.toml index dee84f904..72c3dd6b7 100644 --- a/crates/node-core/Cargo.toml +++ b/crates/node-core/Cargo.toml @@ -96,6 +96,7 @@ procfs = "0.16.0" [dev-dependencies] # test vectors generation proptest.workspace = true +reth-network-types.workspace = true [features] optimism = [ diff --git a/crates/node-core/src/args/network.rs b/crates/node-core/src/args/network.rs index 0ba5d6cd1..a993d07e6 100644 --- a/crates/node-core/src/args/network.rs +++ b/crates/node-core/src/args/network.rs @@ -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, + pub trusted_peers: Vec, /// 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>, + pub bootnodes: Option>, + + /// 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::::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() { diff --git a/crates/node/builder/Cargo.toml b/crates/node/builder/Cargo.toml index 76beb5477..d76d58651 100644 --- a/crates/node/builder/Cargo.toml +++ b/crates/node/builder/Cargo.toml @@ -58,6 +58,7 @@ eyre.workspace = true fdlimit.workspace = true confy.workspace = true rayon.workspace = true +backon.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/node/builder/src/launch/common.rs b/crates/node/builder/src/launch/common.rs index b4f759f25..c58b1b6f1 100644 --- a/crates/node/builder/src/launch/common.rs +++ b/crates/node/builder/src/launch/common.rs @@ -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> { - 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 { + /// + /// This is async because the trusted peers may have to be resolved. + pub async fn load_toml_config(&self, config: &NodeConfig) -> eyre::Result { let config_path = config.config.clone().unwrap_or_else(|| self.data_dir.config()); let mut toml_config = confy::load_path::(&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) diff --git a/crates/node/builder/src/launch/mod.rs b/crates/node/builder/src/launch/mod.rs index 74e09c87a..5a94e42ac 100644 --- a/crates/node/builder/src/launch/mod.rs +++ b/crates/node/builder/src/launch/mod.rs @@ -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 diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index b9a74d7da..b2ec3e886 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -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::{ diff --git a/crates/primitives/src/net.rs b/crates/primitives/src/net.rs index dcb10545f..a70a32e8f 100644 --- a/crates/primitives/src/net.rs +++ b/crates/primitives/src/net.rs @@ -1,4 +1,4 @@ -pub use reth_network_types::{NodeRecord, NodeRecordParseError}; +pub use reth_network_types::{NodeRecord, NodeRecordParseError, TrustedPeer}; // Ethereum bootnodes come from // OP bootnodes come from