chore: move NodeRecord type (#8121)

This commit is contained in:
Matthias Seitz
2024-05-06 17:00:35 +02:00
committed by GitHub
parent b77473cfab
commit 614e1bccd0
11 changed files with 391 additions and 388 deletions

12
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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..])
}