mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-01-29 00:58:11 -05:00
chore: move NodeRecord type (#8121)
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
362
crates/net/types/src/node_record.rs
Normal file
362
crates/net/types/src/node_record.rs
Normal file
@@ -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<Self, Self::Err> {
|
||||
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::<u16>().map_err(NodeRecordParseError::Discport)?
|
||||
} else {
|
||||
port
|
||||
};
|
||||
|
||||
let id = url
|
||||
.username()
|
||||
.parse::<PeerId>()
|
||||
.map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
|
||||
|
||||
Ok(Self { address, id, tcp_port: port, udp_port })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Enr<SecretKey>> for NodeRecord {
|
||||
type Error = NodeRecordParseError;
|
||||
|
||||
fn try_from(enr: &Enr<SecretKey>) -> Result<Self, Self::Error> {
|
||||
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::<NodeRecord>(&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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub use reth_rpc_types::{NodeRecord, NodeRecordParseError};
|
||||
pub use reth_network_types::{NodeRecord, NodeRecordParseError};
|
||||
|
||||
// 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>
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Self, Self::Err> {
|
||||
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::<u16>().map_err(NodeRecordParseError::Discport)?
|
||||
} else {
|
||||
port
|
||||
};
|
||||
|
||||
let id = url
|
||||
.username()
|
||||
.parse::<PeerId>()
|
||||
.map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
|
||||
|
||||
Ok(Self { address, id, tcp_port: port, udp_port })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Enr<SecretKey>> for NodeRecord {
|
||||
type Error = NodeRecordParseError;
|
||||
|
||||
fn try_from(enr: &Enr<SecretKey>) -> Result<Self, Self::Error> {
|
||||
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::<NodeRecord>(&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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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..])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user