Add serde support for NodeRecord primitive type (#617)

* Add serde support to NodeRecord

* Move NodeRecord to primitives

along with NodeKey and Octets

* Reexport NodeRecord from discv4

* Move NodeKey and kad_key back to discv4::node

Also, move NodeRecord::key functionality to a helper function: discv4::node::record_key.
This avoids the discv5 dependency in the primitives crate.

* Fix NodeRecord (de)serializing

The default derive macros work with a dictionary like display.
Changed that to serde_with macros, that use Display and FromStr traits.

* Add some tests for NodeRecord (de)serializing

* Hide NodeKey struct

* Move Octets after NodeRecord

* Replace record_key with From trait

* Fix clippy error

unnecessary into()
This commit is contained in:
Tomás
2022-12-27 14:03:54 -03:00
committed by GitHub
parent c2b19cecef
commit dcd3923d19
14 changed files with 526 additions and 339 deletions

156
Cargo.lock generated
View File

@ -80,6 +80,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
@ -446,8 +455,11 @@ version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"iana-time-zone",
"num-integer",
"num-traits",
"serde",
"winapi",
]
[[package]]
@ -582,6 +594,16 @@ dependencies = [
"syn",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "confy"
version = "0.5.1"
@ -784,6 +806,50 @@ dependencies = [
"zeroize",
]
[[package]]
name = "cxx"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
[[package]]
name = "cxxbridge-macro"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "darling"
version = "0.10.2"
@ -1912,6 +1978,30 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678"
[[package]]
name = "iana-time-zone"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -2014,6 +2104,7 @@ checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
@ -2323,6 +2414,15 @@ dependencies = [
"bytes",
]
[[package]]
name = "link-cplusplus"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
dependencies = [
"cc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@ -3426,7 +3526,6 @@ dependencies = [
"tokio",
"tokio-stream",
"tracing",
"url",
]
[[package]]
@ -3689,16 +3788,20 @@ dependencies = [
"modular-bitfield",
"parity-scale-codec",
"plain_hasher",
"rand 0.8.5",
"reth-codecs",
"reth-rlp",
"reth-rlp-derive",
"secp256k1 0.24.2",
"serde",
"serde_json",
"serde_with",
"sucds",
"test-fuzz",
"thiserror",
"tiny-keccak",
"triehash",
"url",
]
[[package]]
@ -4108,6 +4211,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scratch"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
[[package]]
name = "sct"
version = "0.7.0"
@ -4274,6 +4383,34 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25bf4a5a814902cd1014dbccfa4d4560fb8432c779471e96e035602519f82eef"
dependencies = [
"base64 0.13.1",
"chrono",
"hex",
"indexmap",
"serde",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3452b4c0f6c1e357f73fdb87cd1efabaa12acf328c7a528e252893baeb3f4aa"
dependencies = [
"darling 0.14.2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serial_test"
version = "0.10.0"
@ -4721,8 +4858,10 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
@ -4731,6 +4870,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "time-macros"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@ -5113,6 +5261,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
version = "0.2.4"

View File

@ -31,13 +31,12 @@ tokio-stream = "0.1"
# misc
bytes = "1.2"
generic-array = "0.14"
tracing = "0.1"
thiserror = "1.0"
url = "2.3"
hex = "0.4"
public-ip = "0.2"
rand = { version = "0.8", optional = true }
generic-array = "0.14"
[dev-dependencies]
rand = "0.8"

View File

@ -2,7 +2,7 @@
// <https://github.com/ledgerwatch/erigon/blob/610e648dc43ec8cd6563313e28f06f534a9091b3/params/bootnodes.go>
use crate::node::NodeRecord;
use reth_primitives::NodeRecord;
/// Ethereum Foundation Go Bootnodes
pub static MAINNET_BOOTNODES : [&str; 8] = [

View File

@ -3,9 +3,9 @@
//! This basis of this file has been taken from the discv5 codebase:
//! https://github.com/sigp/discv5
use crate::node::NodeRecord;
use bytes::{Bytes, BytesMut};
use reth_net_common::ban_list::BanList;
use reth_primitives::NodeRecord;
use reth_rlp::Encodable;
use std::{
collections::{HashMap, HashSet},

View File

@ -19,7 +19,6 @@
//! [`DiscoveryUpdate`] that listeners will receive.
use crate::{
error::{DecodePacketError, Discv4Error},
node::{kad_key, NodeKey},
proto::{FindNode, Message, Neighbours, Packet, Ping, Pong},
};
use bytes::{Bytes, BytesMut};
@ -58,8 +57,12 @@ mod proto;
mod config;
pub use config::{Discv4Config, Discv4ConfigBuilder};
mod node;
pub use node::NodeRecord;
use node::{kad_key, NodeKey};
// reexport NodeRecord primitive
pub use reth_primitives::NodeRecord;
#[cfg(any(test, feature = "mock"))]
pub mod mock;
@ -139,8 +142,8 @@ impl Discv4 {
/// use std::str::FromStr;
/// use rand::thread_rng;
/// use secp256k1::SECP256K1;
/// use reth_primitives::PeerId;
/// use reth_discv4::{Discv4, Discv4Config, NodeRecord};
/// use reth_primitives::{NodeRecord, PeerId};
/// use reth_discv4::{Discv4, Discv4Config};
/// # async fn t() -> io::Result<()> {
/// // generate a (random) keypair
/// let mut rng = thread_rng();
@ -384,7 +387,7 @@ impl Discv4Service {
tasks.spawn(async move { send_loop(udp, egress_rx).await });
let kbuckets = KBucketsTable::new(
local_node_record.key(),
NodeKey::from(&local_node_record).into(),
Duration::from_secs(60),
MAX_NODES_PER_BUCKET,
None,
@ -1831,7 +1834,7 @@ mod tests {
fn test_insert() {
let local_node_record = rng_record(&mut rand::thread_rng());
let mut kbuckets: KBucketsTable<NodeKey, NodeEntry> = KBucketsTable::new(
local_node_record.key(),
NodeKey::from(&local_node_record).into(),
Duration::from_secs(60),
MAX_NODES_PER_BUCKET,
None,

View File

@ -3,13 +3,12 @@
#![allow(missing_docs, unused)]
use crate::{
node::NodeRecord,
proto::{FindNode, Message, Neighbours, NodeEndpoint, Packet, Ping, Pong},
receive_loop, send_loop, Discv4, Discv4Config, Discv4Service, EgressSender, IngressEvent,
IngressReceiver, PeerId, SAFE_MAX_DATAGRAM_NEIGHBOUR_RECORDS,
};
use rand::{thread_rng, Rng, RngCore};
use reth_primitives::{hex_literal::hex, ForkHash, ForkId, H256};
use reth_primitives::{hex_literal::hex, ForkHash, ForkId, NodeRecord, H256};
use secp256k1::{SecretKey, SECP256K1};
use std::{
collections::{HashMap, HashSet},

View File

@ -1,18 +1,5 @@
use crate::{proto::Octets, PeerId};
use bytes::{Buf, BufMut};
use generic_array::GenericArray;
use reth_primitives::keccak256;
use reth_rlp::{Decodable, DecodeError, Encodable};
use reth_rlp_derive::RlpEncodable;
use secp256k1::{SecretKey, SECP256K1};
use std::{
fmt,
fmt::Write,
net::{IpAddr, Ipv4Addr, SocketAddr},
num::ParseIntError,
str::FromStr,
};
use url::{Host, Url};
use reth_primitives::{keccak256, NodeRecord, PeerId};
/// The key type for the table.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
@ -32,252 +19,14 @@ impl From<NodeKey> for discv5::Key<NodeKey> {
}
}
impl From<&NodeRecord> for NodeKey {
fn from(node: &NodeRecord) -> Self {
NodeKey(node.id)
}
}
/// Converts a `PeerId` into the required `Key` type for the table
#[inline]
pub(crate) fn kad_key(node: PeerId) -> discv5::Key<NodeKey> {
discv5::kbucket::Key::from(NodeKey::from(node))
}
/// Represents a ENR in discv4.
///
/// Note: this is only an excerpt of the [ENR](enr::Enr) datastructure which is sent in Neighbours
/// message.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
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)
}
/// Creates a new record from a socket addr and peer id.
#[allow(unused)]
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)
}
/// Returns the key type for the kademlia table
#[must_use]
#[inline]
pub(crate) fn key(&self) -> discv5::Key<NodeKey> {
NodeKey(self.id).into()
}
}
impl fmt::Display for NodeRecord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("enode://")?;
hex::encode(self.id.as_bytes()).fmt(f)?;
f.write_char('@')?;
self.address.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 {
#[error("Failed to parse url: {0}")]
InvalidUrl(String),
#[error("Failed to parse id")]
InvalidId(String),
#[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)| {
if maybe_disc.as_ref() == "discport" {
Some(port)
} else {
None
}
}) {
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 Encodable for NodeRecord {
fn encode(&self, out: &mut dyn BufMut) {
#[derive(RlpEncodable)]
struct EncodeNode {
octets: Octets,
udp_port: u16,
tcp_port: u16,
id: PeerId,
}
let octets = match self.address {
IpAddr::V4(addr) => Octets::V4(addr.octets()),
IpAddr::V6(addr) => Octets::V6(addr.octets()),
};
let node =
EncodeNode { octets, udp_port: self.udp_port, tcp_port: self.tcp_port, id: self.id };
node.encode(out)
}
}
impl Decodable for NodeRecord {
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
let b = &mut &**buf;
let rlp_head = reth_rlp::Header::decode(b)?;
if !rlp_head.list {
return Err(DecodeError::UnexpectedString)
}
let started_len = b.len();
let octets = Octets::decode(b)?;
let this = Self {
address: octets.into(),
udp_port: Decodable::decode(b)?,
tcp_port: Decodable::decode(b)?,
id: Decodable::decode(b)?,
};
// the ENR record can contain additional entries that we skip
let consumed = started_len - b.len();
if consumed > rlp_head.payload_length {
return Err(DecodeError::ListLengthMismatch {
expected: rlp_head.payload_length,
got: consumed,
})
}
let rem = rlp_head.payload_length - consumed;
b.advance(rem);
*buf = *b;
Ok(this)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::BytesMut;
use rand::{thread_rng, Rng, RngCore};
#[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: PeerId::random(),
};
let mut buf = BytesMut::new();
record.encode(&mut buf);
let decoded = NodeRecord::decode(&mut buf.as_ref()).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: PeerId::random(),
};
let mut buf = BytesMut::new();
record.encode(&mut buf);
let decoded = NodeRecord::decode(&mut buf.as_ref()).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}"));
}
}

View File

@ -1,16 +1,16 @@
#![allow(missing_docs)]
use crate::{error::DecodePacketError, node::NodeRecord, PeerId, MAX_PACKET_SIZE, MIN_PACKET_SIZE};
use crate::{error::DecodePacketError, PeerId, MAX_PACKET_SIZE, MIN_PACKET_SIZE};
use bytes::{Buf, BufMut, Bytes, BytesMut};
use enr::Enr;
use reth_primitives::{keccak256, ForkId, H256};
use reth_primitives::{keccak256, ForkId, NodeRecord, Octets, H256};
use reth_rlp::{Decodable, DecodeError, Encodable, Header};
use reth_rlp_derive::{RlpDecodable, RlpEncodable};
use secp256k1::{
ecdsa::{RecoverableSignature, RecoveryId},
SecretKey, SECP256K1,
};
use std::net::{IpAddr, Ipv6Addr};
use std::net::IpAddr;
// Note: this is adapted from https://github.com/vorot93/discv4
@ -459,64 +459,6 @@ impl Decodable for Pong {
}
}
/// IpAddr octets
#[derive(Debug, Copy, Clone)]
pub(crate) enum Octets {
V4([u8; 4]),
V6([u8; 16]),
}
impl From<Octets> for IpAddr {
fn from(value: Octets) -> Self {
match value {
Octets::V4(o) => IpAddr::from(o),
Octets::V6(o) => {
let ipv6 = Ipv6Addr::from(o);
// If the ipv6 is ipv4 compatible/mapped, simply return the ipv4.
if let Some(ipv4) = ipv6.to_ipv4() {
IpAddr::V4(ipv4)
} else {
IpAddr::V6(ipv6)
}
}
}
}
}
impl Encodable for Octets {
fn encode(&self, out: &mut dyn BufMut) {
let octets = match self {
Octets::V4(ref o) => &o[..],
Octets::V6(ref o) => &o[..],
};
octets.encode(out)
}
}
impl Decodable for Octets {
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
let h = Header::decode(buf)?;
if h.list {
return Err(DecodeError::UnexpectedList)
}
let o = match h.payload_length {
4 => {
let mut to = [0_u8; 4];
to.copy_from_slice(&buf[..4]);
Octets::V4(to)
}
16 => {
let mut to = [0u8; 16];
to.copy_from_slice(&buf[..16]);
Octets::V6(to)
}
_ => return Err(DecodeError::UnexpectedLength),
};
buf.advance(h.payload_length);
Ok(o)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -5,8 +5,8 @@ use crate::{
peers::PeersConfig,
session::SessionsConfig,
};
use reth_discv4::{Discv4Config, Discv4ConfigBuilder, NodeRecord, DEFAULT_DISCOVERY_PORT};
use reth_primitives::{Chain, ForkFilter, Hardfork, PeerId, H256, MAINNET_GENESIS};
use reth_discv4::{Discv4Config, Discv4ConfigBuilder, DEFAULT_DISCOVERY_PORT};
use reth_primitives::{Chain, ForkFilter, Hardfork, NodeRecord, PeerId, H256, MAINNET_GENESIS};
use reth_tasks::TaskExecutor;
use secp256k1::{SecretKey, SECP256K1};
use std::{

View File

@ -2,8 +2,8 @@
use crate::error::NetworkError;
use futures::StreamExt;
use reth_discv4::{DiscoveryUpdate, Discv4, Discv4Config, NodeRecord};
use reth_primitives::{ForkId, PeerId};
use reth_discv4::{DiscoveryUpdate, Discv4, Discv4Config};
use reth_primitives::{ForkId, NodeRecord, PeerId};
use secp256k1::SecretKey;
use std::{
collections::{hash_map::Entry, HashMap, VecDeque},

View File

@ -7,11 +7,11 @@ use enr::{k256::ecdsa::SigningKey, Enr, EnrPublicKey};
use ethers_core::utils::Geth;
use ethers_providers::{Http, Middleware, Provider};
use futures::StreamExt;
use reth_discv4::{bootnodes::mainnet_nodes, Discv4Config, NodeRecord};
use reth_discv4::{bootnodes::mainnet_nodes, Discv4Config};
use reth_eth_wire::DisconnectReason;
use reth_net_common::ban_list::BanList;
use reth_network::{NetworkConfig, NetworkEvent, NetworkManager, PeersConfig};
use reth_primitives::PeerId;
use reth_primitives::{NodeRecord, PeerId};
use reth_provider::test_utils::TestApi;
use secp256k1::SecretKey;
use std::{collections::HashSet, net::SocketAddr, sync::Arc, time::Duration};

View File

@ -14,6 +14,7 @@ reth-rlp = { path = "../common/rlp", features = [
"derive",
"ethereum-types",
] }
reth-rlp-derive = { path = "../common/rlp-derive" }
reth-codecs = { version = "0.1.0", path = "../storage/codecs" }
# ethereum
@ -34,6 +35,7 @@ crc = "1"
# misc
bytes = "1.2"
serde = "1.0"
serde_with = "2.1.0"
thiserror = "1"
sucds = "0.5.0"
arbitrary = { version = "1.1.7", features = ["derive"], optional = true }
@ -41,6 +43,7 @@ hex = "0.4"
hex-literal = "0.3"
modular-bitfield = "0.11.2"
derive_more = "0.99"
url = "2.3"
# proof related
triehash = "0.8"
@ -53,6 +56,7 @@ arbitrary = { version = "1.1.7", features = ["derive"] }
serde_json = "1.0"
hex-literal = "0.3"
test-fuzz = "3.0.4"
rand = "0.8"
# necessary so we don't hit a "undeclared 'std'":
# https://github.com/paradigmxyz/reth/pull/177#discussion_r1021172198

View File

@ -22,6 +22,7 @@ mod hex_bytes;
mod integer_list;
mod jsonu256;
mod log;
mod net;
mod peer;
mod receipt;
mod storage;
@ -42,6 +43,7 @@ pub use hex_bytes::Bytes;
pub use integer_list::IntegerList;
pub use jsonu256::JsonU256;
pub use log::Log;
pub use net::{NodeRecord, Octets};
pub use peer::{PeerId, WithPeerId};
pub use receipt::Receipt;
pub use storage::StorageEntry;

View File

@ -0,0 +1,335 @@
use crate::PeerId;
use bytes::{Buf, BufMut};
use reth_rlp::{Decodable, DecodeError, Encodable, Header};
use reth_rlp_derive::RlpEncodable;
use secp256k1::{SecretKey, SECP256K1};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::{
fmt,
fmt::Write,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
num::ParseIntError,
str::FromStr,
};
use url::{Host, Url};
/// Represents a ENR in discv4.
///
/// Note: this is only an excerpt of the [ENR](enr::Enr) datastructure which is sent in Neighbours
/// message.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, SerializeDisplay, DeserializeFromStr)]
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)
}
/// Creates a new record from a socket addr and peer id.
#[allow(unused)]
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://")?;
hex::encode(self.id.as_bytes()).fmt(f)?;
f.write_char('@')?;
self.address.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 {
#[error("Failed to parse url: {0}")]
InvalidUrl(String),
#[error("Failed to parse id")]
InvalidId(String),
#[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)| {
if maybe_disc.as_ref() == "discport" {
Some(port)
} else {
None
}
}) {
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 Encodable for NodeRecord {
fn encode(&self, out: &mut dyn BufMut) {
#[derive(RlpEncodable)]
struct EncodeNode {
octets: Octets,
udp_port: u16,
tcp_port: u16,
id: PeerId,
}
let octets = match self.address {
IpAddr::V4(addr) => Octets::V4(addr.octets()),
IpAddr::V6(addr) => Octets::V6(addr.octets()),
};
let node =
EncodeNode { octets, udp_port: self.udp_port, tcp_port: self.tcp_port, id: self.id };
node.encode(out)
}
}
impl Decodable for NodeRecord {
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
let b = &mut &**buf;
let rlp_head = reth_rlp::Header::decode(b)?;
if !rlp_head.list {
return Err(DecodeError::UnexpectedString)
}
let started_len = b.len();
let octets = Octets::decode(b)?;
let this = Self {
address: octets.into(),
udp_port: Decodable::decode(b)?,
tcp_port: Decodable::decode(b)?,
id: Decodable::decode(b)?,
};
// the ENR record can contain additional entries that we skip
let consumed = started_len - b.len();
if consumed > rlp_head.payload_length {
return Err(DecodeError::ListLengthMismatch {
expected: rlp_head.payload_length,
got: consumed,
})
}
let rem = rlp_head.payload_length - consumed;
b.advance(rem);
*buf = *b;
Ok(this)
}
}
/// IpAddr octets
#[derive(Debug, Copy, Clone)]
pub enum Octets {
/// Ipv4 Octet variant
V4([u8; 4]),
/// Ipv6 Octet variant
V6([u8; 16]),
}
impl From<Octets> for IpAddr {
fn from(value: Octets) -> Self {
match value {
Octets::V4(o) => IpAddr::from(o),
Octets::V6(o) => {
let ipv6 = Ipv6Addr::from(o);
// If the ipv6 is ipv4 compatible/mapped, simply return the ipv4.
if let Some(ipv4) = ipv6.to_ipv4() {
IpAddr::V4(ipv4)
} else {
IpAddr::V6(ipv6)
}
}
}
}
}
impl Encodable for Octets {
fn encode(&self, out: &mut dyn BufMut) {
let octets = match self {
Octets::V4(ref o) => &o[..],
Octets::V6(ref o) => &o[..],
};
octets.encode(out)
}
}
impl Decodable for Octets {
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
let h = Header::decode(buf)?;
if h.list {
return Err(DecodeError::UnexpectedList)
}
let o = match h.payload_length {
4 => {
let mut to = [0_u8; 4];
to.copy_from_slice(&buf[..4]);
Octets::V4(to)
}
16 => {
let mut to = [0u8; 16];
to.copy_from_slice(&buf[..16]);
Octets::V6(to)
}
_ => return Err(DecodeError::UnexpectedLength),
};
buf.advance(h.payload_length);
Ok(o)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::BytesMut;
use rand::{thread_rng, Rng, RngCore};
#[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: PeerId::random(),
};
let mut buf = BytesMut::new();
record.encode(&mut buf);
let decoded = NodeRecord::decode(&mut buf.as_ref()).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: PeerId::random(),
};
let mut buf = BytesMut::new();
record.encode(&mut buf);
let decoded = NodeRecord::decode(&mut buf.as_ref()).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 node = NodeRecord{
address: IpAddr::V4([10, 3, 58, 6].into()),
tcp_port: 30303u16,
udp_port: 30301u16,
id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
};
let ser = serde_json::to_string::<NodeRecord>(&node).expect("couldn't serialize");
assert_eq!(ser, "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"")
}
#[test]
fn test_node_deserialize() {
let url = "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"";
let node: NodeRecord = serde_json::from_str(url).expect("couldn't deserialize");
assert_eq!(node, NodeRecord{
address: IpAddr::V4([10, 3, 58, 6].into()),
tcp_port: 30303u16,
udp_port: 30301u16,
id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
})
}
}