mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat(net): add discv4 crate (#113)
* port kad * feat: port kad bucket * feat: add discv4 * chore: rustfmt * cargo update * just reuse discv5 table * test: add rlp tests * message encoding * feat: impl codec roundtrip testing * more work in message handling * implement ping * feat: impl commands * cleanup * more cleanup * trim config * more docs * feat: implement recursive lookup * docs * cleanup config * feat: implement update stream * chore: config cleanup * docs: add crate docs * feat: more testing * fix deny * clarify ring * docs: more docs * use discv5 master * docs: address review and add comments * update readme * rustmft * chore(clippy): make clippy happy
This commit is contained in:
937
Cargo.lock
generated
937
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@ members = [
|
|||||||
"crates/net/p2p",
|
"crates/net/p2p",
|
||||||
"crates/net/ecies",
|
"crates/net/ecies",
|
||||||
"crates/net/eth-wire",
|
"crates/net/eth-wire",
|
||||||
|
"crates/net/discv4",
|
||||||
"crates/net/rpc",
|
"crates/net/rpc",
|
||||||
"crates/net/rpc-api",
|
"crates/net/rpc-api",
|
||||||
"crates/net/rpc-types",
|
"crates/net/rpc-types",
|
||||||
|
|||||||
42
crates/net/discv4/Cargo.toml
Normal file
42
crates/net/discv4/Cargo.toml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
[package]
|
||||||
|
name = "reth-discv4"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
repository = "https://github.com/foundry-rs/reth"
|
||||||
|
readme = "README.md"
|
||||||
|
description = """
|
||||||
|
Ethereum network support
|
||||||
|
"""
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# reth
|
||||||
|
reth-primitives = { path = "../../primitives" }
|
||||||
|
reth-rlp = { path = "../../common/rlp" }
|
||||||
|
reth-rlp-derive = { path = "../../common/rlp-derive" }
|
||||||
|
|
||||||
|
# ethereum
|
||||||
|
discv5 = { git = "https://github.com/sigp/discv5" }
|
||||||
|
secp256k1 = { version = "0.24", features = [
|
||||||
|
"global-context",
|
||||||
|
"rand-std",
|
||||||
|
"recovery",
|
||||||
|
] }
|
||||||
|
|
||||||
|
# async/futures
|
||||||
|
tokio = { version = "1", features = ["io-util", "net", "time"] }
|
||||||
|
tokio-stream = "0.1"
|
||||||
|
|
||||||
|
# misc
|
||||||
|
generic-array = "0.14"
|
||||||
|
tracing = "0.1"
|
||||||
|
bytes = "1.2"
|
||||||
|
thiserror = "1.0"
|
||||||
|
url = "2.3"
|
||||||
|
hex = "0.4"
|
||||||
|
public-ip = "0.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand = "0.8"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tracing-test = "0.2"
|
||||||
22
crates/net/discv4/README.md
Normal file
22
crates/net/discv4/README.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# <h1 align="center"> discv4 </h1>
|
||||||
|
|
||||||
|
This is a rust implementation of
|
||||||
|
the [Discovery v4](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/discv4.md)
|
||||||
|
peer discovery protocol.
|
||||||
|
|
||||||
|
For comparison to Discovery v5,
|
||||||
|
see [discv5#comparison-with-node-discovery-v4](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/discv5/discv5.md#comparison-with-node-discovery-v4)
|
||||||
|
|
||||||
|
This is inspired by the [discv5](https://github.com/sigp/discv5) crate and reuses its kademlia implementation.
|
||||||
|
|
||||||
|
## Finding peers
|
||||||
|
|
||||||
|
The discovery service continuously attempts to connect to other nodes on the network until it has found enough peers.
|
||||||
|
If UPnP (Universal Plug and Play) is supported by the router the service is running on, it will also accept connections
|
||||||
|
from external nodes. In the discovery protocol, nodes exchange information about where the node can be reached to
|
||||||
|
eventually establish RLPx sessions.
|
||||||
|
|
||||||
|
## Trouble Shooting
|
||||||
|
|
||||||
|
The discv4 protocol depends on the local system clock. If the clock is not accurate it can cause connectivity issues
|
||||||
|
because the expiration timestamps might be wrong.
|
||||||
61
crates/net/discv4/src/bootnodes.rs
Normal file
61
crates/net/discv4/src/bootnodes.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
//! Various known bootstrap nodes for networks
|
||||||
|
|
||||||
|
// <https://github.com/ledgerwatch/erigon/blob/610e648dc43ec8cd6563313e28f06f534a9091b3/params/bootnodes.go>
|
||||||
|
|
||||||
|
use crate::node::NodeRecord;
|
||||||
|
|
||||||
|
/// Ethereum Foundation Go Bootnodes
|
||||||
|
pub static MAINNET_BOOTNODES : [&str; 8] = [
|
||||||
|
"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303", // bootnode-aws-ap-southeast-1-001
|
||||||
|
"enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303", // bootnode-aws-us-east-1-001
|
||||||
|
"enode://8499da03c47d637b20eee24eec3c356c9a2e6148d6fe25ca195c7949ab8ec2c03e3556126b0d7ed644675e78c4318b08691b7b57de10e5f0d40d05b09238fa0a@52.187.207.27:30303", // bootnode-azure-australiaeast-001
|
||||||
|
"enode://103858bdb88756c71f15e9b5e09b56dc1be52f0a5021d46301dbbfb7e130029cc9d0d6f73f693bc29b665770fff7da4d34f3c6379fe12721b5d7a0bcb5ca1fc1@191.234.162.198:30303", // bootnode-azure-brazilsouth-001
|
||||||
|
"enode://715171f50508aba88aecd1250af392a45a330af91d7b90701c436b618c86aaa1589c9184561907bebbb56439b8f8787bc01f49a7c77276c58c1b09822d75e8e8@52.231.165.108:30303", // bootnode-azure-koreasouth-001
|
||||||
|
"enode://5d6d7cd20d6da4bb83a1d28cadb5d409b64edf314c0335df658c1a54e32c7c4a7ab7823d57c39b6a757556e68ff1df17c748b698544a55cb488b52479a92b60f@104.42.217.25:30303", // bootnode-azure-westus-001
|
||||||
|
"enode://2b252ab6a1d0f971d9722cb839a42cb81db019ba44c08754628ab4a823487071b5695317c8ccd085219c3a03af063495b2f1da8d18218da2d6a82981b45e6ffc@65.108.70.101:30303", // bootnode-hetzner-hel
|
||||||
|
"enode://4aeb4ab6c14b23e2c4cfdce879c04b0748a20d8e9b59e25ded2a08143e265c6c25936e74cbc8e641e3312ca288673d91f2f93f8e277de3cfa444ecdaaf982052@157.90.35.166:30303", // bootnode-hetzner-fsn
|
||||||
|
];
|
||||||
|
|
||||||
|
/// SEPOLIA BOOTNODES
|
||||||
|
pub static SEPOLIA_BOOTNODES : [&str; 2] = [
|
||||||
|
// geth
|
||||||
|
"enode://9246d00bc8fd1742e5ad2428b80fc4dc45d786283e05ef6edbd9002cbc335d40998444732fbe921cb88e1d2c73d1b1de53bae6a2237996e9bfe14f871baf7066@18.168.182.86:30303",
|
||||||
|
// besu
|
||||||
|
"enode://ec66ddcf1a974950bd4c782789a7e04f8aa7110a72569b6e65fcd51e937e74eed303b1ea734e4d19cfaec9fbff9b6ee65bf31dcb50ba79acce9dd63a6aca61c7@52.14.151.177:30303",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// GOERLI bootnodes
|
||||||
|
pub static GOERLI_BOOTNODES : [&str; 7] = [
|
||||||
|
// Upstream bootnodes
|
||||||
|
"enode://011f758e6552d105183b1761c5e2dea0111bc20fd5f6422bc7f91e0fabbec9a6595caf6239b37feb773dddd3f87240d99d859431891e4a642cf2a0a9e6cbb98a@51.141.78.53:30303",
|
||||||
|
"enode://176b9417f511d05b6b2cf3e34b756cf0a7096b3094572a8f6ef4cdcb9d1f9d00683bf0f83347eebdf3b81c3521c2332086d9592802230bf528eaf606a1d9677b@13.93.54.137:30303",
|
||||||
|
"enode://46add44b9f13965f7b9875ac6b85f016f341012d84f975377573800a863526f4da19ae2c620ec73d11591fa9510e992ecc03ad0751f53cc02f7c7ed6d55c7291@94.237.54.114:30313",
|
||||||
|
"enode://b5948a2d3e9d486c4d75bf32713221c2bd6cf86463302339299bd227dc2e276cd5a1c7ca4f43a0e9122fe9af884efed563bd2a1fd28661f3b5f5ad7bf1de5949@18.218.250.66:30303",
|
||||||
|
|
||||||
|
// Ethereum Foundation bootnode
|
||||||
|
"enode://a61215641fb8714a373c80edbfa0ea8878243193f57c96eeb44d0bc019ef295abd4e044fd619bfc4c59731a73fb79afe84e9ab6da0c743ceb479cbb6d263fa91@3.11.147.67:30303",
|
||||||
|
|
||||||
|
// Goerli Initiative bootnodes
|
||||||
|
"enode://d4f764a48ec2a8ecf883735776fdefe0a3949eb0ca476bd7bc8d0954a9defe8fea15ae5da7d40b5d2d59ce9524a99daedadf6da6283fca492cc80b53689fb3b3@46.4.99.122:32109",
|
||||||
|
"enode://d2b720352e8216c9efc470091aa91ddafc53e222b32780f505c817ceef69e01d5b0b0797b69db254c586f493872352f5a022b4d8479a00fc92ec55f9ad46a27e@88.99.70.182:30303",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Returns parsed mainnet nodes
|
||||||
|
pub fn mainnet_nodes() -> Vec<NodeRecord> {
|
||||||
|
parse_nodes(&MAINNET_BOOTNODES[..])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns parsed goerli nodes
|
||||||
|
pub fn goerli_nodes() -> Vec<NodeRecord> {
|
||||||
|
parse_nodes(&GOERLI_BOOTNODES[..])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns parsed sepolia nodes
|
||||||
|
pub fn sepolia_nodes() -> Vec<NodeRecord> {
|
||||||
|
parse_nodes(&SEPOLIA_BOOTNODES[..])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses all the nodes
|
||||||
|
pub fn parse_nodes(nodes: impl IntoIterator<Item = impl AsRef<str>>) -> Vec<NodeRecord> {
|
||||||
|
nodes.into_iter().map(|s| s.as_ref().parse().unwrap()).collect()
|
||||||
|
}
|
||||||
128
crates/net/discv4/src/config.rs
Normal file
128
crates/net/discv4/src/config.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
use crate::node::NodeRecord;
|
||||||
|
use discv5::PermitBanList;
|
||||||
|
///! A set of configuration parameters to tune the discovery protocol.
|
||||||
|
// This basis of this file has been taken from the discv5 codebase:
|
||||||
|
// https://github.com/sigp/discv5
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Configuration parameters that define the performance of the discovery network.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Discv4Config {
|
||||||
|
/// Whether to enable the incoming packet filter. Default: false.
|
||||||
|
pub enable_packet_filter: bool,
|
||||||
|
/// The number of retries for each UDP request. Default: 1.
|
||||||
|
pub request_retries: u8,
|
||||||
|
/// The time between pings to ensure connectivity amongst connected nodes. Default: 300
|
||||||
|
/// seconds.
|
||||||
|
pub ping_interval: Duration,
|
||||||
|
/// The duration of we consider a ping timed out.
|
||||||
|
pub ping_timeout: Duration,
|
||||||
|
/// The rate at which lookups should be triggered.
|
||||||
|
pub lookup_interval: Duration,
|
||||||
|
/// The duration of we consider a FindNode request timed out.
|
||||||
|
pub find_node_timeout: Duration,
|
||||||
|
/// The duration we set for neighbours responses
|
||||||
|
pub neighbours_timeout: Duration,
|
||||||
|
/// A set of lists that permit or ban IP's or NodeIds from the server. See
|
||||||
|
/// `crate::PermitBanList`.
|
||||||
|
pub permit_ban_list: PermitBanList,
|
||||||
|
/// Set the default duration for which nodes are banned for. This timeouts are checked every 5
|
||||||
|
/// minutes, so the precision will be to the nearest 5 minutes. If set to `None`, bans from
|
||||||
|
/// the filter will last indefinitely. Default is 1 hour.
|
||||||
|
pub ban_duration: Option<Duration>,
|
||||||
|
/// Nodes to boot from.
|
||||||
|
pub bootstrap_nodes: HashSet<NodeRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Discv4Config {
|
||||||
|
/// Returns a new default builder instance
|
||||||
|
pub fn builder() -> Discv4ConfigBuilder {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Discv4Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enable_packet_filter: false,
|
||||||
|
request_retries: 1,
|
||||||
|
ping_interval: Duration::from_secs(300),
|
||||||
|
ping_timeout: Duration::from_secs(5),
|
||||||
|
find_node_timeout: Duration::from_secs(2),
|
||||||
|
neighbours_timeout: Duration::from_secs(30),
|
||||||
|
lookup_interval: Duration::from_secs(20),
|
||||||
|
permit_ban_list: PermitBanList::default(),
|
||||||
|
ban_duration: Some(Duration::from_secs(3600)), // 1 hour
|
||||||
|
bootstrap_nodes: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Discv4ConfigBuilder {
|
||||||
|
config: Discv4Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Discv4ConfigBuilder {
|
||||||
|
/// Whether to enable the incoming packet filter.
|
||||||
|
pub fn enable_packet_filter(&mut self) -> &mut Self {
|
||||||
|
self.config.enable_packet_filter = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of retries for each UDP request.
|
||||||
|
pub fn request_retries(&mut self, retries: u8) -> &mut Self {
|
||||||
|
self.config.request_retries = retries;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time between pings to ensure connectivity amongst connected nodes.
|
||||||
|
pub fn ping_interval(&mut self, interval: Duration) -> &mut Self {
|
||||||
|
self.config.ping_interval = interval;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the timeout for pings
|
||||||
|
pub fn ping_timeout(&mut self, duration: Duration) -> &mut Self {
|
||||||
|
self.config.ping_timeout = duration;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of lists that permit or ban IP's or NodeIds from the server. See
|
||||||
|
/// `crate::PermitBanList`.
|
||||||
|
pub fn permit_ban_list(&mut self, list: PermitBanList) -> &mut Self {
|
||||||
|
self.config.permit_ban_list = list;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the lookup interval duration.
|
||||||
|
pub fn lookup_interval(&mut self, lookup_interval: Duration) -> &mut Self {
|
||||||
|
self.config.lookup_interval = lookup_interval;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the default duration for which nodes are banned for. This timeouts are checked every 5
|
||||||
|
/// minutes, so the precision will be to the nearest 5 minutes. If set to `None`, bans from
|
||||||
|
/// the filter will last indefinitely. Default is 1 hour.
|
||||||
|
pub fn ban_duration(&mut self, ban_duration: Option<Duration>) -> &mut Self {
|
||||||
|
self.config.ban_duration = ban_duration;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a boot node
|
||||||
|
pub fn add_boot_node(&mut self, node: NodeRecord) -> &mut Self {
|
||||||
|
self.config.bootstrap_nodes.insert(node);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds multiple boot nodes
|
||||||
|
pub fn add_boot_nodes(&mut self, nodes: impl IntoIterator<Item = NodeRecord>) -> &mut Self {
|
||||||
|
self.config.bootstrap_nodes.extend(nodes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&mut self) -> Discv4Config {
|
||||||
|
self.config.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
36
crates/net/discv4/src/error.rs
Normal file
36
crates/net/discv4/src/error.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//! Error types that can occur in this crate.
|
||||||
|
|
||||||
|
use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError};
|
||||||
|
|
||||||
|
/// Error thrown when decoding a UDP packet.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum DecodePacketError {
|
||||||
|
#[error("Failed to rlp decode: {0:?}")]
|
||||||
|
Rlp(#[from] reth_rlp::DecodeError),
|
||||||
|
#[error("Received packet len too short.")]
|
||||||
|
PacketTooShort,
|
||||||
|
#[error("Hash of the header not equals to the hash of the data.")]
|
||||||
|
HashMismatch,
|
||||||
|
#[error("Message id {0} is not supported.")]
|
||||||
|
UnknownMessage(u8),
|
||||||
|
#[error("Failed to recover public key: {0:?}")]
|
||||||
|
Secp256k1(#[from] secp256k1::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// High level errors that can occur when interacting with the discovery service
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Discv4Error {
|
||||||
|
/// Failed to send a command over the channel
|
||||||
|
#[error("Failed to send on a closed channel")]
|
||||||
|
Send,
|
||||||
|
/// Failed to receive a command response
|
||||||
|
#[error(transparent)]
|
||||||
|
Receive(#[from] RecvError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<SendError<T>> for Discv4Error {
|
||||||
|
fn from(_: SendError<T>) -> Self {
|
||||||
|
Discv4Error::Send
|
||||||
|
}
|
||||||
|
}
|
||||||
1273
crates/net/discv4/src/lib.rs
Normal file
1273
crates/net/discv4/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
211
crates/net/discv4/src/node.rs
Normal file
211
crates/net/discv4/src/node.rs
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
use crate::{proto::Octets, NodeId};
|
||||||
|
use bytes::{Buf, BufMut};
|
||||||
|
use generic_array::GenericArray;
|
||||||
|
use reth_primitives::keccak256;
|
||||||
|
use reth_rlp::{Decodable, DecodeError, Encodable};
|
||||||
|
use reth_rlp_derive::RlpEncodable;
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
use url::{Host, Url};
|
||||||
|
|
||||||
|
/// The key type for the table.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub(crate) struct NodeKey(pub(crate) NodeId);
|
||||||
|
|
||||||
|
impl From<NodeId> for NodeKey {
|
||||||
|
fn from(value: NodeId) -> Self {
|
||||||
|
NodeKey(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NodeKey> for discv5::Key<NodeKey> {
|
||||||
|
fn from(value: NodeKey) -> Self {
|
||||||
|
let hash = keccak256(value.0.as_bytes());
|
||||||
|
let hash = *GenericArray::from_slice(hash.as_bytes());
|
||||||
|
discv5::Key::new_raw(value, hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a `NodeId` into the required `Key` type for the table
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn kad_key(node: NodeId) -> discv5::Key<NodeKey> {
|
||||||
|
discv5::kbucket::Key::from(NodeKey::from(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a ENR in discv4.
|
||||||
|
#[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: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeRecord {
|
||||||
|
/// Creates a new record
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn new(addr: SocketAddr, id: NodeId) -> 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 id = url
|
||||||
|
.username()
|
||||||
|
.parse::<NodeId>()
|
||||||
|
.map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Self { address, id, tcp_port: port, udp_port: 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: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
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: NodeId::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: NodeId::random(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
record.encode(&mut buf);
|
||||||
|
|
||||||
|
let decoded = NodeRecord::decode(&mut buf.as_ref()).unwrap();
|
||||||
|
assert_eq!(record, decoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
578
crates/net/discv4/src/proto.rs
Normal file
578
crates/net/discv4/src/proto.rs
Normal file
@ -0,0 +1,578 @@
|
|||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
|
use crate::{error::DecodePacketError, node::NodeRecord, NodeId, MAX_PACKET_SIZE, MIN_PACKET_SIZE};
|
||||||
|
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||||
|
use reth_primitives::{keccak256, 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};
|
||||||
|
|
||||||
|
// Note: this is adapted from https://github.com/vorot93/discv4
|
||||||
|
|
||||||
|
/// Id for message variants.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum MessageId {
|
||||||
|
Ping = 1,
|
||||||
|
Pong = 2,
|
||||||
|
FindNode = 3,
|
||||||
|
Neighbours = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageId {
|
||||||
|
/// Converts the byte that represents the message id to the enum.
|
||||||
|
fn from_u8(msg: u8) -> Result<Self, u8> {
|
||||||
|
let msg = match msg {
|
||||||
|
1 => MessageId::Ping,
|
||||||
|
2 => MessageId::Pong,
|
||||||
|
3 => MessageId::FindNode,
|
||||||
|
4 => MessageId::Neighbours,
|
||||||
|
_ => return Err(msg),
|
||||||
|
};
|
||||||
|
Ok(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All message variants
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum Message {
|
||||||
|
Ping(Ping),
|
||||||
|
Pong(Pong),
|
||||||
|
FindNode(FindNode),
|
||||||
|
Neighbours(Neighbours),
|
||||||
|
}
|
||||||
|
|
||||||
|
// === impl Message ===
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
/// Returns the id for this type
|
||||||
|
pub fn msg_type(&self) -> MessageId {
|
||||||
|
match self {
|
||||||
|
Message::Ping(_) => MessageId::Ping,
|
||||||
|
Message::Pong(_) => MessageId::Pong,
|
||||||
|
Message::FindNode(_) => MessageId::FindNode,
|
||||||
|
Message::Neighbours(_) => MessageId::Neighbours,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes the UDP datagram, See <https://github.com/ethereum/devp2p/blob/master/discv4.md#wire-protocol>
|
||||||
|
///
|
||||||
|
/// The datagram is `header || payload`
|
||||||
|
/// where header is `hash || signature || packet-type`
|
||||||
|
pub fn encode(&self, secret_key: &SecretKey) -> (Bytes, H256) {
|
||||||
|
// allocate max packet size
|
||||||
|
let mut datagram = BytesMut::with_capacity(MAX_PACKET_SIZE);
|
||||||
|
|
||||||
|
// since signature has fixed len, we can split and fill the datagram buffer at fixed
|
||||||
|
// positions, this way we can encode the message directly in the datagram buffer
|
||||||
|
let mut sig_bytes = datagram.split_off(H256::len_bytes());
|
||||||
|
let mut payload = sig_bytes.split_off(secp256k1::constants::COMPACT_SIGNATURE_SIZE + 1);
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Message::Ping(message) => {
|
||||||
|
payload.put_u8(1);
|
||||||
|
message.encode(&mut payload);
|
||||||
|
}
|
||||||
|
Message::Pong(message) => {
|
||||||
|
payload.put_u8(2);
|
||||||
|
message.encode(&mut payload);
|
||||||
|
}
|
||||||
|
Message::FindNode(message) => {
|
||||||
|
payload.put_u8(3);
|
||||||
|
message.encode(&mut payload);
|
||||||
|
}
|
||||||
|
Message::Neighbours(message) => {
|
||||||
|
payload.put_u8(4);
|
||||||
|
message.encode(&mut payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature: RecoverableSignature = SECP256K1.sign_ecdsa_recoverable(
|
||||||
|
&secp256k1::Message::from_slice(keccak256(&payload).as_ref())
|
||||||
|
.expect("is correct MESSAGE_SIZE; qed"),
|
||||||
|
secret_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (rec, sig) = signature.serialize_compact();
|
||||||
|
sig_bytes.extend_from_slice(&sig);
|
||||||
|
sig_bytes.put_u8(rec.to_i32() as u8);
|
||||||
|
sig_bytes.unsplit(payload);
|
||||||
|
|
||||||
|
let hash = keccak256(&sig_bytes);
|
||||||
|
datagram.extend_from_slice(hash.as_bytes());
|
||||||
|
|
||||||
|
datagram.unsplit(sig_bytes);
|
||||||
|
(datagram.freeze(), hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes the [`Message`] from the given buffer.
|
||||||
|
///
|
||||||
|
/// Returns the decoded message and the public key of the sender.
|
||||||
|
pub fn decode(packet: &[u8]) -> Result<Packet, DecodePacketError> {
|
||||||
|
if packet.len() < MIN_PACKET_SIZE {
|
||||||
|
return Err(DecodePacketError::PacketTooShort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parses the wire-protocol, every packet starts with a header:
|
||||||
|
// packet-header = hash || signature || packet-type
|
||||||
|
// hash = keccak256(signature || packet-type || packet-data)
|
||||||
|
// signature = sign(packet-type || packet-data)
|
||||||
|
|
||||||
|
let header_hash = keccak256(&packet[32..]);
|
||||||
|
let data_hash = H256::from_slice(&packet[..32]);
|
||||||
|
if data_hash != header_hash {
|
||||||
|
return Err(DecodePacketError::HashMismatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature = &packet[32..96];
|
||||||
|
let recovery_id = RecoveryId::from_i32(packet[96] as i32)?;
|
||||||
|
let recoverable_sig = RecoverableSignature::from_compact(signature, recovery_id)?;
|
||||||
|
|
||||||
|
// recover the public key
|
||||||
|
let msg = secp256k1::Message::from_slice(keccak256(&packet[97..]).as_bytes())?;
|
||||||
|
|
||||||
|
let pk = SECP256K1.recover_ecdsa(&msg, &recoverable_sig)?;
|
||||||
|
let node_id = NodeId::from_slice(&pk.serialize_uncompressed()[1..]);
|
||||||
|
|
||||||
|
let msg_type = packet[97];
|
||||||
|
let payload = &mut &packet[98..];
|
||||||
|
|
||||||
|
let msg = match MessageId::from_u8(msg_type).map_err(DecodePacketError::UnknownMessage)? {
|
||||||
|
MessageId::Ping => Message::Ping(Ping::decode(payload)?),
|
||||||
|
MessageId::Pong => Message::Pong(Pong::decode(payload)?),
|
||||||
|
MessageId::FindNode => Message::FindNode(FindNode::decode(payload)?),
|
||||||
|
MessageId::Neighbours => Message::Neighbours(Neighbours::decode(payload)?),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Packet { msg, node_id, hash: header_hash })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decoded packet
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Packet {
|
||||||
|
pub msg: Message,
|
||||||
|
pub node_id: NodeId,
|
||||||
|
pub hash: H256,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the `from`, `to` fields in the packets
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct NodeEndpoint {
|
||||||
|
pub address: IpAddr,
|
||||||
|
pub udp_port: u16,
|
||||||
|
pub tcp_port: u16,
|
||||||
|
}
|
||||||
|
impl Decodable for NodeEndpoint {
|
||||||
|
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
|
||||||
|
let b = &mut &**buf;
|
||||||
|
let rlp_head = 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)?,
|
||||||
|
};
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encodable for NodeEndpoint {
|
||||||
|
fn encode(&self, out: &mut dyn BufMut) {
|
||||||
|
#[derive(RlpEncodable)]
|
||||||
|
struct RlpEndpoint {
|
||||||
|
octets: Octets,
|
||||||
|
udp_port: u16,
|
||||||
|
tcp_port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
let octets = match self.address {
|
||||||
|
IpAddr::V4(addr) => Octets::V4(addr.octets()),
|
||||||
|
IpAddr::V6(addr) => Octets::V6(addr.octets()),
|
||||||
|
};
|
||||||
|
let p = RlpEndpoint { octets, udp_port: self.udp_port, tcp_port: self.tcp_port };
|
||||||
|
p.encode(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NodeRecord> for NodeEndpoint {
|
||||||
|
fn from(NodeRecord { address, tcp_port, udp_port, .. }: NodeRecord) -> Self {
|
||||||
|
Self { address, tcp_port, udp_port }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [FindNode packet](https://github.com/ethereum/devp2p/blob/master/discv4.md#findnode-packet-0x03).).
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, RlpEncodable, RlpDecodable)]
|
||||||
|
pub struct FindNode {
|
||||||
|
pub id: NodeId,
|
||||||
|
pub expire: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [Neighbours packet](https://github.com/ethereum/devp2p/blob/master/discv4.md#neighbors-packet-0x04).
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, RlpEncodable, RlpDecodable)]
|
||||||
|
pub struct Neighbours {
|
||||||
|
pub nodes: Vec<NodeRecord>,
|
||||||
|
pub expire: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [Ping packet](https://github.com/ethereum/devp2p/blob/master/discv4.md#ping-packet-0x01).
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Ping {
|
||||||
|
pub from: NodeEndpoint,
|
||||||
|
pub to: NodeEndpoint,
|
||||||
|
pub expire: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encodable for Ping {
|
||||||
|
fn encode(&self, out: &mut dyn BufMut) {
|
||||||
|
#[derive(RlpEncodable)]
|
||||||
|
struct V4PingMessage<'a> {
|
||||||
|
version: u32,
|
||||||
|
from: &'a NodeEndpoint,
|
||||||
|
to: &'a NodeEndpoint,
|
||||||
|
expire: u64,
|
||||||
|
}
|
||||||
|
V4PingMessage {
|
||||||
|
version: 4, // version 4
|
||||||
|
from: &self.from,
|
||||||
|
to: &self.to,
|
||||||
|
expire: self.expire,
|
||||||
|
}
|
||||||
|
.encode(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decodable for Ping {
|
||||||
|
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
|
||||||
|
let b = &mut &**buf;
|
||||||
|
let rlp_head = Header::decode(b)?;
|
||||||
|
if !rlp_head.list {
|
||||||
|
return Err(DecodeError::UnexpectedString)
|
||||||
|
}
|
||||||
|
let started_len = b.len();
|
||||||
|
let _version = u32::decode(b)?;
|
||||||
|
let this = Self {
|
||||||
|
from: Decodable::decode(b)?,
|
||||||
|
to: Decodable::decode(b)?,
|
||||||
|
expire: Decodable::decode(b)?,
|
||||||
|
};
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [Pong packet](https://github.com/ethereum/devp2p/blob/master/discv4.md#pong-packet-0x02).
|
||||||
|
// #[derive(Clone, Debug, Eq, PartialEq, RlpEncodable, RlpDecodable)]
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, RlpEncodable)]
|
||||||
|
pub struct Pong {
|
||||||
|
pub to: NodeEndpoint,
|
||||||
|
pub echo: H256,
|
||||||
|
pub expire: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decodable for Pong {
|
||||||
|
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
|
||||||
|
let b = &mut &**buf;
|
||||||
|
let rlp_head = Header::decode(b)?;
|
||||||
|
if !rlp_head.list {
|
||||||
|
return Err(DecodeError::UnexpectedString)
|
||||||
|
}
|
||||||
|
let started_len = b.len();
|
||||||
|
let this = Self {
|
||||||
|
to: Decodable::decode(b)?,
|
||||||
|
echo: Decodable::decode(b)?,
|
||||||
|
expire: Decodable::decode(b)?,
|
||||||
|
};
|
||||||
|
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(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::*;
|
||||||
|
use crate::SAFE_MAX_DATAGRAM_NEIGHBOUR_RECORDS;
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use rand::{thread_rng, Rng, RngCore};
|
||||||
|
|
||||||
|
fn rng_endpoint(rng: &mut impl Rng) -> NodeEndpoint {
|
||||||
|
let address = if rng.gen() {
|
||||||
|
let mut ip = [0u8; 4];
|
||||||
|
rng.fill_bytes(&mut ip);
|
||||||
|
IpAddr::V4(ip.into())
|
||||||
|
} else {
|
||||||
|
let mut ip = [0u8; 16];
|
||||||
|
rng.fill_bytes(&mut ip);
|
||||||
|
IpAddr::V6(ip.into())
|
||||||
|
};
|
||||||
|
NodeEndpoint { address, tcp_port: rng.gen(), udp_port: rng.gen() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rng_record(rng: &mut impl RngCore) -> NodeRecord {
|
||||||
|
let NodeEndpoint { address, udp_port, tcp_port } = rng_endpoint(rng);
|
||||||
|
NodeRecord { address, tcp_port, udp_port, id: NodeId::random() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rng_ipv6_record(rng: &mut impl RngCore) -> NodeRecord {
|
||||||
|
let mut ip = [0u8; 16];
|
||||||
|
rng.fill_bytes(&mut ip);
|
||||||
|
let address = IpAddr::V6(ip.into());
|
||||||
|
NodeRecord { address, tcp_port: rng.gen(), udp_port: rng.gen(), id: NodeId::random() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rng_ipv4_record(rng: &mut impl RngCore) -> NodeRecord {
|
||||||
|
let mut ip = [0u8; 4];
|
||||||
|
rng.fill_bytes(&mut ip);
|
||||||
|
let address = IpAddr::V4(ip.into());
|
||||||
|
NodeRecord { address, tcp_port: rng.gen(), udp_port: rng.gen(), id: NodeId::random() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rng_message(rng: &mut impl RngCore) -> Message {
|
||||||
|
match rng.gen_range(1..=4) {
|
||||||
|
1 => Message::Ping(Ping {
|
||||||
|
from: rng_endpoint(rng),
|
||||||
|
to: rng_endpoint(rng),
|
||||||
|
expire: rng.gen(),
|
||||||
|
}),
|
||||||
|
2 => Message::Pong(Pong {
|
||||||
|
to: rng_endpoint(rng),
|
||||||
|
echo: H256::random(),
|
||||||
|
expire: rng.gen(),
|
||||||
|
}),
|
||||||
|
3 => Message::FindNode(FindNode { id: NodeId::random(), expire: rng.gen() }),
|
||||||
|
4 => {
|
||||||
|
let num: usize = rng.gen_range(1..=SAFE_MAX_DATAGRAM_NEIGHBOUR_RECORDS);
|
||||||
|
Message::Neighbours(Neighbours {
|
||||||
|
nodes: std::iter::repeat_with(|| rng_record(rng)).take(num).collect(),
|
||||||
|
expire: rng.gen(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_ipv_v4() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
for _ in 0..100 {
|
||||||
|
let mut ip = [0u8; 4];
|
||||||
|
rng.fill_bytes(&mut ip);
|
||||||
|
let msg = NodeEndpoint {
|
||||||
|
address: IpAddr::V4(ip.into()),
|
||||||
|
tcp_port: rng.gen(),
|
||||||
|
udp_port: rng.gen(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
msg.encode(&mut buf);
|
||||||
|
|
||||||
|
let decoded = NodeEndpoint::decode(&mut buf.as_ref()).unwrap();
|
||||||
|
assert_eq!(msg, decoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_endpoint_ipv_64() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
for _ in 0..100 {
|
||||||
|
let mut ip = [0u8; 16];
|
||||||
|
rng.fill_bytes(&mut ip);
|
||||||
|
let msg = NodeEndpoint {
|
||||||
|
address: IpAddr::V6(ip.into()),
|
||||||
|
tcp_port: rng.gen(),
|
||||||
|
udp_port: rng.gen(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
msg.encode(&mut buf);
|
||||||
|
|
||||||
|
let decoded = NodeEndpoint::decode(&mut buf.as_ref()).unwrap();
|
||||||
|
assert_eq!(msg, decoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ping_message() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
for _ in 0..100 {
|
||||||
|
let mut ip = [0u8; 16];
|
||||||
|
rng.fill_bytes(&mut ip);
|
||||||
|
let msg = Ping { from: rng_endpoint(&mut rng), to: rng_endpoint(&mut rng), expire: 0 };
|
||||||
|
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
msg.encode(&mut buf);
|
||||||
|
|
||||||
|
let decoded = Ping::decode(&mut buf.as_ref()).unwrap();
|
||||||
|
assert_eq!(msg, decoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash_mismatch() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let msg = rng_message(&mut rng);
|
||||||
|
let (secret_key, _) = SECP256K1.generate_keypair(&mut rng);
|
||||||
|
let (buf, _) = msg.encode(&secret_key);
|
||||||
|
let mut buf = BytesMut::from(buf.as_ref());
|
||||||
|
buf.put_u8(0);
|
||||||
|
match Message::decode(buf.as_ref()).unwrap_err() {
|
||||||
|
DecodePacketError::HashMismatch => {}
|
||||||
|
err => {
|
||||||
|
unreachable!("unexpected err {}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn neighbours_max_nodes() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
for _ in 0..1000 {
|
||||||
|
let msg = Message::Neighbours(Neighbours {
|
||||||
|
nodes: std::iter::repeat_with(|| rng_ipv6_record(&mut rng))
|
||||||
|
.take(SAFE_MAX_DATAGRAM_NEIGHBOUR_RECORDS)
|
||||||
|
.collect(),
|
||||||
|
expire: rng.gen(),
|
||||||
|
});
|
||||||
|
let (secret_key, _) = SECP256K1.generate_keypair(&mut rng);
|
||||||
|
|
||||||
|
let (encoded, _) = msg.encode(&secret_key);
|
||||||
|
assert!(encoded.len() <= MAX_PACKET_SIZE, "{} {:?}", encoded.len(), msg);
|
||||||
|
|
||||||
|
let mut neighbours = Neighbours {
|
||||||
|
nodes: std::iter::repeat_with(|| rng_ipv6_record(&mut rng))
|
||||||
|
.take(SAFE_MAX_DATAGRAM_NEIGHBOUR_RECORDS - 1)
|
||||||
|
.collect(),
|
||||||
|
expire: rng.gen(),
|
||||||
|
};
|
||||||
|
neighbours.nodes.push(rng_ipv4_record(&mut rng));
|
||||||
|
let msg = Message::Neighbours(neighbours);
|
||||||
|
let (encoded, _) = msg.encode(&secret_key);
|
||||||
|
assert!(encoded.len() <= MAX_PACKET_SIZE, "{} {:?}", encoded.len(), msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode_message() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
for _ in 0..100 {
|
||||||
|
let msg = rng_message(&mut rng);
|
||||||
|
let (secret_key, pk) = SECP256K1.generate_keypair(&mut rng);
|
||||||
|
let sender_id = NodeId::from_slice(&pk.serialize_uncompressed()[1..]);
|
||||||
|
|
||||||
|
let (buf, _) = msg.encode(&secret_key);
|
||||||
|
|
||||||
|
let packet = Message::decode(buf.as_ref()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(msg, packet.msg);
|
||||||
|
assert_eq!(sender_id, packet.node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_pong_packet() {
|
||||||
|
let packet = "2ad84c37327a06c2522cf7bc039621da89f68907441b755935bb308dc4cd17d6fe550e90329ad6a516ca7db18e08900067928a0dfa3b5c75d55a42c984497373698d98616662c048983ea85895ea2da765eabeb15525478384e106337bfd8ed50002f3c9843ed8cae682fd1c80a008ad4dead0922211df47593e7d837b2b23d13954285871ca23250ea594993ded84635690e5829670";
|
||||||
|
let data = hex::decode(packet).unwrap();
|
||||||
|
Message::decode(&data).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn decode_ping_packet() {
|
||||||
|
let packet = "05ae5bf922cf2a93f97632a4ab0943dc252a0dab0c42d86dd62e5d91e1a0966e9b628fbf4763fdfbb928540460b797e6be2e7058a82f6083f6d2e7391bb021741459976d4152aa16bbee0c3609dcfac6668db1ef78b7ee9f8b4ced10dd5ae2900101df04cb8403d12d4f82765f82765fc9843ed8cae6828aa6808463569916829670";
|
||||||
|
let data = hex::decode(packet).unwrap();
|
||||||
|
Message::decode(&data).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -54,7 +54,7 @@ pub type StorageValue = H256;
|
|||||||
// NOTE: There is a benefit of using wrapped Bytes as it gives us serde and debug
|
// NOTE: There is a benefit of using wrapped Bytes as it gives us serde and debug
|
||||||
pub use ethers_core::{
|
pub use ethers_core::{
|
||||||
types as rpc,
|
types as rpc,
|
||||||
types::{Bloom, Bytes, H128, H160, H256, H512, H64, U128, U256, U64},
|
types::{BigEndianHash, Bloom, Bytes, H128, H160, H256, H512, H64, U128, U256, U64},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|||||||
Reference in New Issue
Block a user