diff --git a/Cargo.lock b/Cargo.lock index 4fa6fc73d0..14a5dd752b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7229,10 +7229,14 @@ name = "reth-network-types" version = "0.2.0-beta.6" dependencies = [ "alloy-primitives", + "alloy-rlp", "enr", - "reth-rpc-types", + "rand 0.8.5", "secp256k1", + "serde_json", "serde_with", + "thiserror", + "url", ] [[package]] @@ -7558,6 +7562,7 @@ dependencies = [ "rayon", "reth-codecs", "reth-ethereum-forks", + "reth-network-types", "reth-rpc-types", "revm", "revm-primitives", @@ -7798,29 +7803,24 @@ dependencies = [ name = "reth-rpc-types" version = "0.2.0-beta.6" dependencies = [ - "alloy-genesis 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=17c5650)", "alloy-primitives", - "alloy-rlp", "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=17c5650)", "alloy-rpc-types-anvil", "alloy-rpc-types-engine", "alloy-rpc-types-trace", "arbitrary", "bytes", - "enr", "ethereum_ssz", "ethereum_ssz_derive", "jsonrpsee-types", "proptest", "proptest-derive", "rand 0.8.5", - "secp256k1", "serde", "serde_json", "serde_with", "similar-asserts", "thiserror", - "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e48db17129..dc693e94fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,6 +338,7 @@ smallvec = "1" dyn-clone = "1.0.17" sha2 = { version = "0.10", default-features = false } paste = "1.0" +url = "2.3" # proc-macros proc-macro2 = "1.0" diff --git a/crates/net/types/Cargo.toml b/crates/net/types/Cargo.toml index 9092236b1a..9be9a2f3a2 100644 --- a/crates/net/types/Cargo.toml +++ b/crates/net/types/Cargo.toml @@ -12,17 +12,22 @@ description = "Network types and utils" workspace = true [dependencies] -# reth -reth-rpc-types.workspace = true - -alloy-primitives.workspace = true # eth +alloy-primitives = { workspace = true, features = ["rlp"] } +alloy-rlp = { workspace = true, features = ["derive"] } enr.workspace = true # crypto -secp256k1 = { workspace = true, features = ["global-context", "recovery", "rand"] } +secp256k1.workspace = true # misc serde_with.workspace = true +thiserror.workspace = true +url.workspace = true +[dev-dependencies] +alloy-primitives = { workspace = true, features = ["rand"] } +rand.workspace = true +secp256k1 = { workspace = true, features = ["rand"] } +serde_json.workspace = true diff --git a/crates/net/types/src/lib.rs b/crates/net/types/src/lib.rs index 8d75af9333..e4b9f28a4f 100644 --- a/crates/net/types/src/lib.rs +++ b/crates/net/types/src/lib.rs @@ -11,12 +11,18 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +use alloy_primitives::B512; use secp256k1::{constants::UNCOMPRESSED_PUBLIC_KEY_SIZE, PublicKey, SecretKey}; use std::{net::IpAddr, str::FromStr}; // Re-export PeerId for ease of use. pub use enr::Enr; -pub use reth_rpc_types::{NodeRecord, PeerId}; + +/// Alias for a peer identifier +pub type PeerId = B512; + +pub mod node_record; +pub use node_record::{NodeRecord, NodeRecordParseError}; /// This tag should be set to indicate to libsecp256k1 that the following bytes denote an /// uncompressed pubkey. diff --git a/crates/net/types/src/node_record.rs b/crates/net/types/src/node_record.rs new file mode 100644 index 0000000000..5a6706201a --- /dev/null +++ b/crates/net/types/src/node_record.rs @@ -0,0 +1,362 @@ +//! Commonly used NodeRecord type for peers. + +use std::{ + fmt, + fmt::Write, + net::{IpAddr, Ipv4Addr, SocketAddr}, + num::ParseIntError, + str::FromStr, +}; + +use crate::{pk2id, PeerId}; +use alloy_rlp::{RlpDecodable, RlpEncodable}; +use enr::Enr; +use secp256k1::{SecretKey, SECP256K1}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; + +/// Represents a ENR in discovery. +/// +/// Note: this is only an excerpt of the [`NodeRecord`] data structure. +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + Hash, + SerializeDisplay, + DeserializeFromStr, + RlpEncodable, + RlpDecodable, +)] +pub struct NodeRecord { + /// The Address of a node. + pub address: IpAddr, + /// 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 NodeRecord { + /// Derive the [`NodeRecord`] from the secret key and addr + pub fn from_secret_key(addr: SocketAddr, sk: &SecretKey) -> Self { + let pk = secp256k1::PublicKey::from_secret_key(SECP256K1, sk); + let id = PeerId::from_slice(&pk.serialize_uncompressed()[1..]); + Self::new(addr, id) + } + + /// Converts the `address` into an [`Ipv4Addr`] if the `address` is a mapped + /// [Ipv6Addr](std::net::Ipv6Addr). + /// + /// Returns `true` if the address was converted. + /// + /// See also [std::net::Ipv6Addr::to_ipv4_mapped] + pub fn convert_ipv4_mapped(&mut self) -> bool { + // convert IPv4 mapped IPv6 address + if let IpAddr::V6(v6) = self.address { + if let Some(v4) = v6.to_ipv4_mapped() { + self.address = v4.into(); + return true + } + } + false + } + + /// Same as [Self::convert_ipv4_mapped] but consumes the type + pub fn into_ipv4_mapped(mut self) -> Self { + self.convert_ipv4_mapped(); + self + } + + /// Creates a new record from a socket addr and peer id. + #[allow(dead_code)] + pub fn new(addr: SocketAddr, id: PeerId) -> Self { + Self { address: addr.ip(), tcp_port: addr.port(), udp_port: addr.port(), id } + } + + /// The TCP socket address of this node + #[must_use] + pub fn tcp_addr(&self) -> SocketAddr { + SocketAddr::new(self.address, self.tcp_port) + } + + /// The UDP socket address of this node + #[must_use] + pub fn udp_addr(&self) -> SocketAddr { + SocketAddr::new(self.address, self.udp_port) + } +} + +impl fmt::Display for NodeRecord { + 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('@')?; + match self.address { + IpAddr::V4(ip) => { + ip.fmt(f)?; + } + IpAddr::V6(ip) => { + // encapsulate with brackets + f.write_char('[')?; + ip.fmt(f)?; + f.write_char(']')?; + } + } + 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 NodeRecord { + type Err = NodeRecordParseError; + + fn from_str(s: &str) -> Result { + use url::{Host, Url}; + + let url = Url::parse(s).map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?; + + let address = match url.host() { + Some(Host::Ipv4(ip)) => IpAddr::V4(ip), + Some(Host::Ipv6(ip)) => IpAddr::V6(ip), + Some(Host::Domain(ip)) => IpAddr::V4( + Ipv4Addr::from_str(ip) + .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?, + ), + _ => return Err(NodeRecordParseError::InvalidUrl(format!("invalid host: {url:?}"))), + }; + 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 { address, id, tcp_port: port, udp_port }) + } +} + +impl TryFrom<&Enr> for NodeRecord { + type Error = NodeRecordParseError; + + fn try_from(enr: &Enr) -> Result { + let Some(address) = enr.ip4().map(IpAddr::from).or_else(|| enr.ip6().map(IpAddr::from)) + else { + return Err(NodeRecordParseError::InvalidUrl("ip missing".to_string())) + }; + + let Some(udp_port) = enr.udp4().or_else(|| enr.udp6()) else { + return Err(NodeRecordParseError::InvalidUrl("udp port missing".to_string())) + }; + + let Some(tcp_port) = enr.tcp4().or_else(|| enr.tcp6()) else { + return Err(NodeRecordParseError::InvalidUrl("tcp port missing".to_string())) + }; + + let id = pk2id(&enr.public_key()); + + Ok(NodeRecord { address, tcp_port, udp_port, id }.into_ipv4_mapped()) + } +} + +#[cfg(test)] +mod tests { + use std::net::Ipv6Addr; + + use alloy_rlp::Decodable; + use rand::{thread_rng, Rng, RngCore}; + + use super::*; + + #[test] + fn test_mapped_ipv6() { + let mut rng = thread_rng(); + + let v4: Ipv4Addr = "0.0.0.0".parse().unwrap(); + let v6 = v4.to_ipv6_mapped(); + + let record = NodeRecord { + address: v6.into(), + tcp_port: rng.gen(), + udp_port: rng.gen(), + id: rng.gen(), + }; + + assert!(record.clone().convert_ipv4_mapped()); + assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4)); + } + + #[test] + fn test_mapped_ipv4() { + let mut rng = thread_rng(); + let v4: Ipv4Addr = "0.0.0.0".parse().unwrap(); + + let record = NodeRecord { + address: v4.into(), + tcp_port: rng.gen(), + udp_port: rng.gen(), + id: rng.gen(), + }; + + assert!(!record.clone().convert_ipv4_mapped()); + assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4)); + } + + #[test] + fn test_noderecord_codec_ipv4() { + let mut rng = thread_rng(); + for _ in 0..100 { + let mut ip = [0u8; 4]; + rng.fill_bytes(&mut ip); + let record = NodeRecord { + address: IpAddr::V4(ip.into()), + tcp_port: rng.gen(), + udp_port: rng.gen(), + id: rng.gen(), + }; + + let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap(); + assert_eq!(record, decoded); + } + } + + #[test] + fn test_noderecord_codec_ipv6() { + let mut rng = thread_rng(); + for _ in 0..100 { + let mut ip = [0u8; 16]; + rng.fill_bytes(&mut ip); + let record = NodeRecord { + address: IpAddr::V6(ip.into()), + tcp_port: rng.gen(), + udp_port: rng.gen(), + id: rng.gen(), + }; + + let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap(); + assert_eq!(record, decoded); + } + } + + #[test] + fn test_url_parse() { + let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301"; + let node: NodeRecord = url.parse().unwrap(); + assert_eq!(node, NodeRecord { + address: IpAddr::V4([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: NodeRecord = 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: NodeRecord = url.parse().unwrap(); + assert_eq!(url, &format!("{node}")); + } + + #[test] + fn test_node_serialize() { + let cases = vec![ + // IPv4 + ( + NodeRecord { + address: IpAddr::V4([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 + ( + NodeRecord { + address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(), + tcp_port: 52150u16, + udp_port: 52151u16, + id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(), + }, + "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]: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\"", + NodeRecord { + address: IpAddr::V4([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\"", + NodeRecord { + address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(), + tcp_port: 52150u16, + udp_port: 52151u16, + id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(), + } + ), + ]; + + for (url, expected) in cases { + let node: NodeRecord = serde_json::from_str(url).expect("couldn't deserialize"); + assert_eq!(node, expected); + } + } +} diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index deaee23006..675c7167f6 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -15,6 +15,7 @@ workspace = true # reth reth-codecs.workspace = true reth-ethereum-forks.workspace = true +reth-network-types.workspace = true reth-rpc-types.workspace = true revm.workspace = true revm-primitives = { workspace = true, features = ["serde"] } diff --git a/crates/primitives/src/net.rs b/crates/primitives/src/net.rs index 068e47e5b2..778e2658bc 100644 --- a/crates/primitives/src/net.rs +++ b/crates/primitives/src/net.rs @@ -1,4 +1,4 @@ -pub use reth_rpc_types::{NodeRecord, NodeRecordParseError}; +pub use reth_network_types::{NodeRecord, NodeRecordParseError}; // Ethereum bootnodes come from // OP bootnodes come from diff --git a/crates/rpc/rpc-types/Cargo.toml b/crates/rpc/rpc-types/Cargo.toml index 5f87e9482d..1426b50f86 100644 --- a/crates/rpc/rpc-types/Cargo.toml +++ b/crates/rpc/rpc-types/Cargo.toml @@ -12,8 +12,8 @@ description = "Reth RPC types" workspace = true [dependencies] + # ethereum -alloy-rlp = { workspace = true, features = ["arrayvec", "derive"] } alloy-primitives = { workspace = true, features = ["rand", "rlp", "serde"] } alloy-rpc-types = { workspace = true, features = ["jsonrpsee-types"] } alloy-rpc-types-anvil.workspace = true @@ -21,8 +21,6 @@ alloy-rpc-types-trace.workspace = true alloy-rpc-types-engine = { workspace = true, features = ["jsonrpsee-types"] } ethereum_ssz_derive = { version = "0.5", optional = true } ethereum_ssz = { version = "0.5", optional = true } -alloy-genesis.workspace = true -enr = { workspace = true, features = ["serde", "rust-secp256k1"] } # misc thiserror.workspace = true @@ -30,19 +28,10 @@ serde = { workspace = true, features = ["derive"] } serde_with = "3.3" serde_json.workspace = true jsonrpsee-types = { workspace = true, optional = true } -url = "2.3" -# necessary so we don't hit a "undeclared 'std'": -# https://github.com/paradigmxyz/reth/pull/177#discussion_r1021172198 -secp256k1.workspace = true - -# arbitrary -arbitrary = { workspace = true, features = ["derive"], optional = true } -proptest = { workspace = true, optional = true } -proptest-derive = { workspace = true, optional = true } [features] default = ["jsonrpsee-types"] -arbitrary = ["dep:arbitrary", "dep:proptest-derive", "dep:proptest", "alloy-primitives/arbitrary", "alloy-rpc-types/arbitrary"] +arbitrary = ["alloy-primitives/arbitrary", "alloy-rpc-types/arbitrary"] ssz = ["dep:ethereum_ssz" ,"dep:ethereum_ssz_derive", "alloy-primitives/ssz", "alloy-rpc-types/ssz", "alloy-rpc-types-engine/ssz"] diff --git a/crates/rpc/rpc-types/src/lib.rs b/crates/rpc/rpc-types/src/lib.rs index 0adcab0f33..01ed0f9110 100644 --- a/crates/rpc/rpc-types/src/lib.rs +++ b/crates/rpc/rpc-types/src/lib.rs @@ -8,6 +8,7 @@ issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/" )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] pub mod beacon; mod eth; diff --git a/crates/rpc/rpc-types/src/net.rs b/crates/rpc/rpc-types/src/net.rs index d72d00fa5e..b434bcbf84 100644 --- a/crates/rpc/rpc-types/src/net.rs +++ b/crates/rpc/rpc-types/src/net.rs @@ -1,19 +1,5 @@ -use crate::{pk_to_id, PeerId}; -use alloy_rlp::{RlpDecodable, RlpEncodable}; use alloy_rpc_types::admin::EthProtocolInfo; -use enr::Enr; -use secp256k1::{SecretKey, SECP256K1}; use serde::{Deserialize, Serialize}; -use serde_with::{DeserializeFromStr, SerializeDisplay}; -use std::{ - fmt, - fmt::Write, - net::{IpAddr, Ipv4Addr, SocketAddr}, - num::ParseIntError, - str::FromStr, -}; -use thiserror::Error; -use url::{Host, Url}; /// The status of the network being ran by the local node. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -25,346 +11,3 @@ pub struct NetworkStatus { /// Information about the Ethereum Wire Protocol. pub eth_protocol_info: EthProtocolInfo, } - -/// Represents a ENR in discovery. -/// -/// Note: this is only an excerpt of the [`NodeRecord`] data structure. -#[derive( - Clone, - Copy, - Debug, - Eq, - PartialEq, - Hash, - SerializeDisplay, - DeserializeFromStr, - RlpEncodable, - RlpDecodable, -)] -pub struct NodeRecord { - /// The Address of a node. - pub address: IpAddr, - /// 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 NodeRecord { - /// Derive the [`NodeRecord`] from the secret key and addr - pub fn from_secret_key(addr: SocketAddr, sk: &SecretKey) -> Self { - let pk = secp256k1::PublicKey::from_secret_key(SECP256K1, sk); - let id = PeerId::from_slice(&pk.serialize_uncompressed()[1..]); - Self::new(addr, id) - } - - /// Converts the `address` into an [`Ipv4Addr`] if the `address` is a mapped - /// [Ipv6Addr](std::net::Ipv6Addr). - /// - /// Returns `true` if the address was converted. - /// - /// See also [std::net::Ipv6Addr::to_ipv4_mapped] - pub fn convert_ipv4_mapped(&mut self) -> bool { - // convert IPv4 mapped IPv6 address - if let IpAddr::V6(v6) = self.address { - if let Some(v4) = v6.to_ipv4_mapped() { - self.address = v4.into(); - return true - } - } - false - } - - /// Same as [Self::convert_ipv4_mapped] but consumes the type - pub fn into_ipv4_mapped(mut self) -> Self { - self.convert_ipv4_mapped(); - self - } - - /// Creates a new record from a socket addr and peer id. - #[allow(dead_code)] - pub fn new(addr: SocketAddr, id: PeerId) -> Self { - Self { address: addr.ip(), tcp_port: addr.port(), udp_port: addr.port(), id } - } - - /// The TCP socket address of this node - #[must_use] - pub fn tcp_addr(&self) -> SocketAddr { - SocketAddr::new(self.address, self.tcp_port) - } - - /// The UDP socket address of this node - #[must_use] - pub fn udp_addr(&self) -> SocketAddr { - SocketAddr::new(self.address, self.udp_port) - } -} - -impl fmt::Display for NodeRecord { - 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('@')?; - match self.address { - IpAddr::V4(ip) => { - ip.fmt(f)?; - } - IpAddr::V6(ip) => { - // encapsulate with brackets - f.write_char('[')?; - ip.fmt(f)?; - f.write_char(']')?; - } - } - 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, 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 NodeRecord { - type Err = NodeRecordParseError; - - fn from_str(s: &str) -> Result { - let url = Url::parse(s).map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?; - - let address = match url.host() { - Some(Host::Ipv4(ip)) => IpAddr::V4(ip), - Some(Host::Ipv6(ip)) => IpAddr::V6(ip), - Some(Host::Domain(ip)) => IpAddr::V4( - Ipv4Addr::from_str(ip) - .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?, - ), - _ => return Err(NodeRecordParseError::InvalidUrl(format!("invalid host: {url:?}"))), - }; - 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 { address, id, tcp_port: port, udp_port }) - } -} - -impl TryFrom<&Enr> for NodeRecord { - type Error = NodeRecordParseError; - - fn try_from(enr: &Enr) -> Result { - let Some(address) = enr.ip4().map(IpAddr::from).or_else(|| enr.ip6().map(IpAddr::from)) - else { - return Err(NodeRecordParseError::InvalidUrl("ip missing".to_string())) - }; - - let Some(udp_port) = enr.udp4().or_else(|| enr.udp6()) else { - return Err(NodeRecordParseError::InvalidUrl("udp port missing".to_string())) - }; - - let Some(tcp_port) = enr.tcp4().or_else(|| enr.tcp6()) else { - return Err(NodeRecordParseError::InvalidUrl("tcp port missing".to_string())) - }; - - let id = pk_to_id(&enr.public_key()); - - Ok(NodeRecord { address, tcp_port, udp_port, id }.into_ipv4_mapped()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_rlp::Decodable; - use rand::{thread_rng, Rng, RngCore}; - use std::net::Ipv6Addr; - - #[test] - fn test_mapped_ipv6() { - let mut rng = thread_rng(); - - let v4: Ipv4Addr = "0.0.0.0".parse().unwrap(); - let v6 = v4.to_ipv6_mapped(); - - let record = NodeRecord { - address: v6.into(), - tcp_port: rng.gen(), - udp_port: rng.gen(), - id: rng.gen(), - }; - - assert!(record.clone().convert_ipv4_mapped()); - assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4)); - } - - #[test] - fn test_mapped_ipv4() { - let mut rng = thread_rng(); - let v4: Ipv4Addr = "0.0.0.0".parse().unwrap(); - - let record = NodeRecord { - address: v4.into(), - tcp_port: rng.gen(), - udp_port: rng.gen(), - id: rng.gen(), - }; - - assert!(!record.clone().convert_ipv4_mapped()); - assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4)); - } - - #[test] - fn test_noderecord_codec_ipv4() { - let mut rng = thread_rng(); - for _ in 0..100 { - let mut ip = [0u8; 4]; - rng.fill_bytes(&mut ip); - let record = NodeRecord { - address: IpAddr::V4(ip.into()), - tcp_port: rng.gen(), - udp_port: rng.gen(), - id: rng.gen(), - }; - - let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap(); - assert_eq!(record, decoded); - } - } - - #[test] - fn test_noderecord_codec_ipv6() { - let mut rng = thread_rng(); - for _ in 0..100 { - let mut ip = [0u8; 16]; - rng.fill_bytes(&mut ip); - let record = NodeRecord { - address: IpAddr::V6(ip.into()), - tcp_port: rng.gen(), - udp_port: rng.gen(), - id: rng.gen(), - }; - - let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap(); - assert_eq!(record, decoded); - } - } - - #[test] - fn test_url_parse() { - let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301"; - let node: NodeRecord = url.parse().unwrap(); - assert_eq!(node, NodeRecord { - address: IpAddr::V4([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: NodeRecord = 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: NodeRecord = url.parse().unwrap(); - assert_eq!(url, &format!("{node}")); - } - - #[test] - fn test_node_serialize() { - let cases = vec![ - // IPv4 - ( - NodeRecord{ - address: IpAddr::V4([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 - ( - NodeRecord{ - address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(), - tcp_port: 52150u16, - udp_port: 52151u16, - id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(), - }, - "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]: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\"", - NodeRecord{ - address: IpAddr::V4([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\"", - NodeRecord{ - address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(), - tcp_port: 52150u16, - udp_port: 52151u16, - id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(), - } - ), - ]; - - for (url, expected) in cases { - let node: NodeRecord = serde_json::from_str(url).expect("couldn't deserialize"); - assert_eq!(node, expected); - } - } -} diff --git a/crates/rpc/rpc-types/src/peer.rs b/crates/rpc/rpc-types/src/peer.rs index 44dbe5d71f..a07e61d002 100644 --- a/crates/rpc/rpc-types/src/peer.rs +++ b/crates/rpc/rpc-types/src/peer.rs @@ -2,8 +2,3 @@ use alloy_primitives::B512; /// Alias for a peer identifier pub type PeerId = B512; - -/// Converts a [`secp256k1::PublicKey`] to a [`PeerId`]. -pub fn pk_to_id(pk: &secp256k1::PublicKey) -> PeerId { - PeerId::from_slice(&pk.serialize_uncompressed()[1..]) -}