mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
883 lines
31 KiB
Rust
883 lines
31 KiB
Rust
//! Discovery v4 protocol implementation.
|
|
|
|
use crate::{error::DecodePacketError, EnrForkIdEntry, PeerId, MAX_PACKET_SIZE, MIN_PACKET_SIZE};
|
|
use alloy_rlp::{
|
|
length_of_length, Decodable, Encodable, Error as RlpError, Header, RlpDecodable, RlpEncodable,
|
|
};
|
|
use enr::{Enr, EnrKey};
|
|
use reth_primitives::{
|
|
bytes::{Buf, BufMut, Bytes, BytesMut},
|
|
keccak256, pk2id, ForkId, NodeRecord, B256,
|
|
};
|
|
use secp256k1::{
|
|
ecdsa::{RecoverableSignature, RecoveryId},
|
|
SecretKey, SECP256K1,
|
|
};
|
|
use std::net::IpAddr;
|
|
|
|
// Note: this is adapted from https://github.com/vorot93/discv4
|
|
|
|
/// Represents the identifier for message variants.
|
|
///
|
|
/// This enumeration assigns unique identifiers (u8 values) to different message types.
|
|
#[derive(Debug)]
|
|
#[repr(u8)]
|
|
pub enum MessageId {
|
|
/// Ping message identifier.
|
|
Ping = 1,
|
|
/// Pong message identifier.
|
|
Pong = 2,
|
|
/// Find node message identifier.
|
|
FindNode = 3,
|
|
/// Neighbours message identifier.
|
|
Neighbours = 4,
|
|
/// ENR request message identifier.
|
|
EnrRequest = 5,
|
|
/// ENR response message identifier.
|
|
EnrResponse = 6,
|
|
}
|
|
|
|
impl MessageId {
|
|
/// Converts the byte that represents the message id to the enum.
|
|
fn from_u8(msg: u8) -> Result<Self, u8> {
|
|
Ok(match msg {
|
|
1 => MessageId::Ping,
|
|
2 => MessageId::Pong,
|
|
3 => MessageId::FindNode,
|
|
4 => MessageId::Neighbours,
|
|
5 => MessageId::EnrRequest,
|
|
6 => MessageId::EnrResponse,
|
|
_ => return Err(msg),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Enum representing various message types exchanged in the Discovery v4 protocol.
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
pub enum Message {
|
|
/// Represents a ping message sent during liveness checks.
|
|
Ping(Ping),
|
|
/// Represents a pong message, which is a reply to a PING message.
|
|
Pong(Pong),
|
|
/// Represents a query for nodes in the given bucket.
|
|
FindNode(FindNode),
|
|
/// Represents a neighbour message, providing information about nearby nodes.
|
|
Neighbours(Neighbours),
|
|
/// Represents an ENR request message, a request for Ethereum Node Records (ENR) as per [EIP-778](https://eips.ethereum.org/EIPS/eip-778).
|
|
EnrRequest(EnrRequest),
|
|
/// Represents an ENR response message, a response to an ENR request with Ethereum Node Records (ENR) as per [EIP-778](https://eips.ethereum.org/EIPS/eip-778).
|
|
EnrResponse(EnrResponse),
|
|
}
|
|
|
|
// === 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,
|
|
Message::EnrRequest(_) => MessageId::EnrRequest,
|
|
Message::EnrResponse(_) => MessageId::EnrResponse,
|
|
}
|
|
}
|
|
|
|
/// 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, B256) {
|
|
// 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(B256::len_bytes());
|
|
let mut payload = sig_bytes.split_off(secp256k1::constants::COMPACT_SIGNATURE_SIZE + 1);
|
|
|
|
// Put the message type at the beginning of the payload
|
|
payload.put_u8(self.msg_type() as u8);
|
|
|
|
// Match the message type and encode the corresponding message into the payload
|
|
match self {
|
|
Message::Ping(message) => message.encode(&mut payload),
|
|
Message::Pong(message) => message.encode(&mut payload),
|
|
Message::FindNode(message) => message.encode(&mut payload),
|
|
Message::Neighbours(message) => message.encode(&mut payload),
|
|
Message::EnrRequest(message) => message.encode(&mut payload),
|
|
Message::EnrResponse(message) => message.encode(&mut payload),
|
|
}
|
|
|
|
// Sign the payload with the secret key using recoverable ECDSA
|
|
let signature: RecoverableSignature = SECP256K1.sign_ecdsa_recoverable(
|
|
&secp256k1::Message::from_slice(keccak256(&payload).as_ref())
|
|
.expect("B256.len() == MESSAGE_SIZE"),
|
|
secret_key,
|
|
);
|
|
|
|
// Serialize the signature and append it to the signature bytes
|
|
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);
|
|
|
|
// Calculate the hash of the signature bytes and append it to the datagram
|
|
let hash = keccak256(&sig_bytes);
|
|
datagram.extend_from_slice(hash.as_slice());
|
|
|
|
// Append the signature bytes to the datagram
|
|
datagram.unsplit(sig_bytes);
|
|
|
|
// Return the frozen datagram and the hash
|
|
(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 = B256::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_slice())?;
|
|
|
|
let pk = SECP256K1.recover_ecdsa(&msg, &recoverable_sig)?;
|
|
let node_id = pk2id(&pk);
|
|
|
|
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)?),
|
|
MessageId::EnrRequest => Message::EnrRequest(EnrRequest::decode(payload)?),
|
|
MessageId::EnrResponse => Message::EnrResponse(EnrResponse::decode(payload)?),
|
|
};
|
|
|
|
Ok(Packet { msg, node_id, hash: header_hash })
|
|
}
|
|
}
|
|
|
|
/// Represents a decoded packet.
|
|
///
|
|
/// This struct holds information about a decoded packet, including the message, node ID, and hash.
|
|
#[derive(Debug)]
|
|
pub struct Packet {
|
|
/// The decoded message from the packet.
|
|
pub msg: Message,
|
|
/// The ID of the peer that sent the packet.
|
|
pub node_id: PeerId,
|
|
/// The hash of the packet.
|
|
pub hash: B256,
|
|
}
|
|
|
|
/// Represents the `from`, `to` fields in the packets
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable)]
|
|
pub struct NodeEndpoint {
|
|
/// The IP address of the network endpoint. It can be either IPv4 or IPv6.
|
|
pub address: IpAddr,
|
|
/// The UDP port used for communication in the discovery protocol.
|
|
pub udp_port: u16,
|
|
/// The TCP port used for communication in the RLPx protocol.
|
|
pub tcp_port: u16,
|
|
}
|
|
|
|
impl From<NodeRecord> for NodeEndpoint {
|
|
fn from(NodeRecord { address, tcp_port, udp_port, .. }: NodeRecord) -> Self {
|
|
Self { address, tcp_port, udp_port }
|
|
}
|
|
}
|
|
|
|
impl NodeEndpoint {
|
|
/// Creates a new [`NodeEndpoint`] from a given UDP address and TCP port.
|
|
pub fn from_udp_address(udp_address: &std::net::SocketAddr, tcp_port: u16) -> Self {
|
|
NodeEndpoint { address: udp_address.ip(), udp_port: udp_address.port(), tcp_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 {
|
|
/// The target node's ID, a 64-byte secp256k1 public key.
|
|
pub id: PeerId,
|
|
/// The expiration timestamp of the packet, an absolute UNIX time stamp.
|
|
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 {
|
|
/// The list of nodes containing IP, UDP port, TCP port, and node ID.
|
|
pub nodes: Vec<NodeRecord>,
|
|
/// The expiration timestamp of the packet, an absolute UNIX time stamp.
|
|
pub expire: u64,
|
|
}
|
|
|
|
/// Passthrough newtype to [`Enr`].
|
|
///
|
|
/// We need to wrap the ENR type because of Rust's orphan rules not allowing
|
|
/// implementing a foreign trait on a foreign type.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct EnrWrapper<K: EnrKey>(Enr<K>);
|
|
|
|
impl<K: EnrKey> EnrWrapper<K> {
|
|
/// Creates a new instance of [`EnrWrapper`].
|
|
pub fn new(enr: Enr<K>) -> Self {
|
|
EnrWrapper(enr)
|
|
}
|
|
}
|
|
|
|
impl<K> Encodable for EnrWrapper<K>
|
|
where
|
|
K: EnrKey,
|
|
{
|
|
fn encode(&self, out: &mut dyn BufMut) {
|
|
let payload_length = self.0.signature().length() +
|
|
self.0.seq().length() +
|
|
self.0.iter().fold(0, |acc, (k, v)| acc + k.as_slice().length() + v.len());
|
|
|
|
let header = Header { list: true, payload_length };
|
|
header.encode(out);
|
|
|
|
self.0.signature().encode(out);
|
|
self.0.seq().encode(out);
|
|
|
|
for (k, v) in self.0.iter() {
|
|
// Keys are byte data
|
|
k.as_slice().encode(out);
|
|
// Values are raw RLP encoded data
|
|
out.put_slice(v);
|
|
}
|
|
}
|
|
|
|
fn length(&self) -> usize {
|
|
let payload_length = self.0.signature().length() +
|
|
self.0.seq().length() +
|
|
self.0.iter().fold(0, |acc, (k, v)| acc + k.as_slice().length() + v.len());
|
|
payload_length + length_of_length(payload_length)
|
|
}
|
|
}
|
|
|
|
fn to_alloy_rlp_error(e: rlp::DecoderError) -> RlpError {
|
|
match e {
|
|
rlp::DecoderError::RlpIsTooShort => RlpError::InputTooShort,
|
|
rlp::DecoderError::RlpInvalidLength => RlpError::Overflow,
|
|
rlp::DecoderError::RlpExpectedToBeList => RlpError::UnexpectedString,
|
|
rlp::DecoderError::RlpExpectedToBeData => RlpError::UnexpectedList,
|
|
rlp::DecoderError::RlpDataLenWithZeroPrefix |
|
|
rlp::DecoderError::RlpListLenWithZeroPrefix => RlpError::LeadingZero,
|
|
rlp::DecoderError::RlpInvalidIndirection => RlpError::NonCanonicalSize,
|
|
rlp::DecoderError::RlpIncorrectListLen => {
|
|
RlpError::Custom("incorrect list length when decoding rlp")
|
|
}
|
|
rlp::DecoderError::RlpIsTooBig => RlpError::Custom("rlp is too big"),
|
|
rlp::DecoderError::RlpInconsistentLengthAndData => {
|
|
RlpError::Custom("inconsistent length and data when decoding rlp")
|
|
}
|
|
rlp::DecoderError::Custom(s) => RlpError::Custom(s),
|
|
}
|
|
}
|
|
|
|
impl<K: EnrKey> Decodable for EnrWrapper<K> {
|
|
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
|
let enr = <Enr<K> as rlp::Decodable>::decode(&rlp::Rlp::new(buf))
|
|
.map_err(to_alloy_rlp_error)
|
|
.map(EnrWrapper::new);
|
|
if enr.is_ok() {
|
|
// Decode was successful, advance buffer
|
|
let header = Header::decode(buf)?;
|
|
buf.advance(header.payload_length);
|
|
}
|
|
enr
|
|
}
|
|
}
|
|
|
|
/// A [ENRRequest packet](https://github.com/ethereum/devp2p/blob/master/discv4.md#enrrequest-packet-0x05).
|
|
///
|
|
/// This packet is used to request the current version of a node's Ethereum Node Record (ENR).
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, RlpEncodable, RlpDecodable)]
|
|
pub struct EnrRequest {
|
|
/// The expiration timestamp for the request. No reply should be sent if it refers to a time in
|
|
/// the past.
|
|
pub expire: u64,
|
|
}
|
|
|
|
/// A [ENRResponse packet](https://github.com/ethereum/devp2p/blob/master/discv4.md#enrresponse-packet-0x06).
|
|
///
|
|
/// This packet is used to respond to an ENRRequest packet and includes the requested ENR along with
|
|
/// the hash of the original request.
|
|
#[derive(Clone, Debug, Eq, PartialEq, RlpEncodable)]
|
|
pub struct EnrResponse {
|
|
/// The hash of the ENRRequest packet being replied to.
|
|
pub request_hash: B256,
|
|
/// The ENR (Ethereum Node Record) for the responding node.
|
|
pub enr: EnrWrapper<SecretKey>,
|
|
}
|
|
|
|
// === impl EnrResponse ===
|
|
|
|
impl EnrResponse {
|
|
/// Returns the [`ForkId`] if set
|
|
///
|
|
/// See also <https://github.com/ethereum/go-ethereum/blob/9244d5cd61f3ea5a7645fdf2a1a96d53421e412f/eth/protocols/eth/discovery.go#L36>
|
|
pub fn eth_fork_id(&self) -> Option<ForkId> {
|
|
let mut maybe_fork_id = self.enr.0.get_raw_rlp(b"eth")?;
|
|
EnrForkIdEntry::decode(&mut maybe_fork_id).ok().map(|entry| entry.fork_id)
|
|
}
|
|
}
|
|
|
|
impl Decodable for EnrResponse {
|
|
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
|
let b = &mut &**buf;
|
|
let rlp_head = Header::decode(b)?;
|
|
if !rlp_head.list {
|
|
return Err(RlpError::UnexpectedString)
|
|
}
|
|
// let started_len = b.len();
|
|
let this = Self {
|
|
request_hash: alloy_rlp::Decodable::decode(b)?,
|
|
enr: EnrWrapper::<SecretKey>::decode(b)?,
|
|
};
|
|
// TODO: `Decodable` can be derived once we have native alloy_rlp decoding for ENR: <https://github.com/paradigmxyz/reth/issues/482>
|
|
// Skipping the size check here is fine since the `buf` is the UDP datagram
|
|
// let consumed = started_len - b.len();
|
|
// if consumed != rlp_head.payload_length {
|
|
// return Err(alloy_rlp::Error::ListLengthMismatch {
|
|
// expected: rlp_head.payload_length,
|
|
// got: consumed,
|
|
// })
|
|
// }
|
|
*buf = *b;
|
|
Ok(this)
|
|
}
|
|
}
|
|
|
|
/// Represents a Ping packet.
|
|
///
|
|
/// A [Ping packet](https://github.com/ethereum/devp2p/blob/master/discv4.md#ping-packet-0x01).
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub struct Ping {
|
|
/// The sender's endpoint.
|
|
pub from: NodeEndpoint,
|
|
/// The recipient's endpoint.
|
|
pub to: NodeEndpoint,
|
|
/// The expiration timestamp.
|
|
pub expire: u64,
|
|
/// Optional enr_seq for <https://eips.ethereum.org/EIPS/eip-868>
|
|
pub enr_sq: Option<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,
|
|
}
|
|
|
|
#[derive(RlpEncodable)]
|
|
struct V4PingMessageEIP868<'a> {
|
|
version: u32,
|
|
from: &'a NodeEndpoint,
|
|
to: &'a NodeEndpoint,
|
|
expire: u64,
|
|
enr_seq: u64,
|
|
}
|
|
if let Some(enr_seq) = self.enr_sq {
|
|
V4PingMessageEIP868 {
|
|
version: 4, // version 4
|
|
from: &self.from,
|
|
to: &self.to,
|
|
expire: self.expire,
|
|
enr_seq,
|
|
}
|
|
.encode(out);
|
|
} else {
|
|
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]) -> alloy_rlp::Result<Self> {
|
|
let b = &mut &**buf;
|
|
let rlp_head = Header::decode(b)?;
|
|
if !rlp_head.list {
|
|
return Err(RlpError::UnexpectedString)
|
|
}
|
|
let started_len = b.len();
|
|
|
|
// > Implementations should ignore any mismatches in version:
|
|
// <https://github.com/ethereum/devp2p/blob/master/discv4.md#ping-packet-0x01>
|
|
let _version = u32::decode(b)?;
|
|
|
|
let mut this = Self {
|
|
from: Decodable::decode(b)?,
|
|
to: Decodable::decode(b)?,
|
|
expire: Decodable::decode(b)?,
|
|
enr_sq: None,
|
|
};
|
|
|
|
// only decode the ENR sequence if there's more data in the datagram to decode else skip
|
|
if b.has_remaining() {
|
|
this.enr_sq = Some(Decodable::decode(b)?);
|
|
}
|
|
|
|
let consumed = started_len - b.len();
|
|
if consumed > rlp_head.payload_length {
|
|
return Err(RlpError::ListLengthMismatch {
|
|
expected: rlp_head.payload_length,
|
|
got: consumed,
|
|
})
|
|
}
|
|
let rem = rlp_head.payload_length - consumed;
|
|
b.advance(rem);
|
|
*buf = *b;
|
|
Ok(this)
|
|
}
|
|
}
|
|
|
|
/// Represents a Pong packet.
|
|
///
|
|
/// A [Pong packet](https://github.com/ethereum/devp2p/blob/master/discv4.md#pong-packet-0x02).
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct Pong {
|
|
/// The recipient's endpoint.
|
|
pub to: NodeEndpoint,
|
|
/// The hash of the corresponding ping packet.
|
|
pub echo: B256,
|
|
/// The expiration timestamp.
|
|
pub expire: u64,
|
|
/// Optional enr_seq for <https://eips.ethereum.org/EIPS/eip-868>
|
|
pub enr_sq: Option<u64>,
|
|
}
|
|
|
|
impl Encodable for Pong {
|
|
fn encode(&self, out: &mut dyn BufMut) {
|
|
#[derive(RlpEncodable)]
|
|
struct PongMessageEIP868<'a> {
|
|
to: &'a NodeEndpoint,
|
|
echo: &'a B256,
|
|
expire: u64,
|
|
enr_seq: u64,
|
|
}
|
|
|
|
#[derive(RlpEncodable)]
|
|
struct PongMessage<'a> {
|
|
to: &'a NodeEndpoint,
|
|
echo: &'a B256,
|
|
expire: u64,
|
|
}
|
|
|
|
if let Some(enr_seq) = self.enr_sq {
|
|
PongMessageEIP868 { to: &self.to, echo: &self.echo, expire: self.expire, enr_seq }
|
|
.encode(out);
|
|
} else {
|
|
PongMessage { to: &self.to, echo: &self.echo, expire: self.expire }.encode(out);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Decodable for Pong {
|
|
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
|
let b = &mut &**buf;
|
|
let rlp_head = Header::decode(b)?;
|
|
if !rlp_head.list {
|
|
return Err(RlpError::UnexpectedString)
|
|
}
|
|
let started_len = b.len();
|
|
let mut this = Self {
|
|
to: Decodable::decode(b)?,
|
|
echo: Decodable::decode(b)?,
|
|
expire: Decodable::decode(b)?,
|
|
enr_sq: None,
|
|
};
|
|
|
|
// only decode the ENR sequence if there's more data in the datagram to decode else skip
|
|
if b.has_remaining() {
|
|
this.enr_sq = Some(Decodable::decode(b)?);
|
|
}
|
|
|
|
let consumed = started_len - b.len();
|
|
if consumed > rlp_head.payload_length {
|
|
return Err(RlpError::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 crate::{
|
|
test_utils::{rng_endpoint, rng_ipv4_record, rng_ipv6_record, rng_message},
|
|
DEFAULT_DISCOVERY_PORT, SAFE_MAX_DATAGRAM_NEIGHBOUR_RECORDS,
|
|
};
|
|
use enr::EnrPublicKey;
|
|
use rand::{thread_rng, Rng, RngCore};
|
|
use reth_primitives::{hex, ForkHash};
|
|
|
|
#[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 decoded = NodeEndpoint::decode(&mut alloy_rlp::encode(msg).as_slice()).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 decoded = NodeEndpoint::decode(&mut alloy_rlp::encode(msg).as_slice()).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,
|
|
enr_sq: None,
|
|
};
|
|
|
|
let decoded = Ping::decode(&mut alloy_rlp::encode(&msg).as_slice()).unwrap();
|
|
assert_eq!(msg, decoded);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_ping_message_with_enr() {
|
|
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,
|
|
enr_sq: Some(rng.gen()),
|
|
};
|
|
|
|
let decoded = Ping::decode(&mut alloy_rlp::encode(&msg).as_slice()).unwrap();
|
|
assert_eq!(msg, decoded);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_pong_message() {
|
|
let mut rng = thread_rng();
|
|
for _ in 0..100 {
|
|
let mut ip = [0u8; 16];
|
|
rng.fill_bytes(&mut ip);
|
|
let msg = Pong {
|
|
to: rng_endpoint(&mut rng),
|
|
echo: rng.gen(),
|
|
expire: rng.gen(),
|
|
enr_sq: None,
|
|
};
|
|
|
|
let decoded = Pong::decode(&mut alloy_rlp::encode(&msg).as_slice()).unwrap();
|
|
assert_eq!(msg, decoded);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_pong_message_with_enr() {
|
|
let mut rng = thread_rng();
|
|
for _ in 0..100 {
|
|
let mut ip = [0u8; 16];
|
|
rng.fill_bytes(&mut ip);
|
|
let msg = Pong {
|
|
to: rng_endpoint(&mut rng),
|
|
echo: rng.gen(),
|
|
expire: rng.gen(),
|
|
enr_sq: Some(rng.gen()),
|
|
};
|
|
|
|
let decoded = Pong::decode(&mut alloy_rlp::encode(&msg).as_slice()).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_vec = buf.to_vec();
|
|
buf_vec.push(0);
|
|
match Message::decode(buf_vec.as_slice()).unwrap_err() {
|
|
DecodePacketError::HashMismatch => {}
|
|
err => {
|
|
unreachable!("unexpected err {}", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn neighbours_max_ipv4() {
|
|
let mut rng = thread_rng();
|
|
let msg = Message::Neighbours(Neighbours {
|
|
nodes: std::iter::repeat_with(|| rng_ipv4_record(&mut rng)).take(16).collect(),
|
|
expire: rng.gen(),
|
|
});
|
|
let (secret_key, _) = SECP256K1.generate_keypair(&mut rng);
|
|
|
|
let (encoded, _) = msg.encode(&secret_key);
|
|
// Assert that 16 nodes never fit into one packet
|
|
assert!(encoded.len() > MAX_PACKET_SIZE, "{} {msg:?}", encoded.len());
|
|
}
|
|
|
|
#[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, "{} {msg:?}", encoded.len());
|
|
|
|
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, "{} {msg:?}", encoded.len());
|
|
}
|
|
}
|
|
|
|
#[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 = pk2id(&pk);
|
|
|
|
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();
|
|
}
|
|
|
|
#[test]
|
|
fn encode_decode_enr_msg() {
|
|
use self::EnrWrapper;
|
|
use alloy_rlp::Decodable;
|
|
use enr::secp256k1::SecretKey;
|
|
use std::net::Ipv4Addr;
|
|
|
|
let mut rng = rand::rngs::OsRng;
|
|
let key = SecretKey::new(&mut rng);
|
|
let ip = Ipv4Addr::new(127, 0, 0, 1);
|
|
let tcp = 3000;
|
|
|
|
let fork_id: ForkId = ForkId { hash: ForkHash([220, 233, 108, 45]), next: 0u64 };
|
|
|
|
let enr = {
|
|
let mut builder = Enr::builder();
|
|
builder.ip(ip.into());
|
|
builder.tcp4(tcp);
|
|
let mut buf = Vec::new();
|
|
let forkentry = EnrForkIdEntry { fork_id };
|
|
forkentry.encode(&mut buf);
|
|
builder.add_value_rlp("eth", buf.into());
|
|
EnrWrapper::new(builder.build(&key).unwrap())
|
|
};
|
|
|
|
let enr_response = EnrResponse { request_hash: rng.gen(), enr };
|
|
|
|
let mut buf = Vec::new();
|
|
enr_response.encode(&mut buf);
|
|
|
|
let decoded = EnrResponse::decode(&mut &buf[..]).unwrap();
|
|
|
|
let fork_id_decoded = decoded.eth_fork_id().unwrap();
|
|
assert_eq!(fork_id, fork_id_decoded);
|
|
}
|
|
|
|
// test vector from the enr library rlp encoding tests
|
|
// <https://github.com/sigp/enr/blob/e59dcb45ea07e423a7091d2a6ede4ad6d8ef2840/src/lib.rs#L1019>
|
|
|
|
#[test]
|
|
fn encode_known_rlp_enr() {
|
|
use self::EnrWrapper;
|
|
use alloy_rlp::Decodable;
|
|
use enr::{secp256k1::SecretKey, EnrPublicKey};
|
|
use std::net::Ipv4Addr;
|
|
|
|
let valid_record =
|
|
hex!("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f"
|
|
);
|
|
let signature =
|
|
hex!("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c"
|
|
);
|
|
let expected_pubkey =
|
|
hex!("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138");
|
|
|
|
let enr = EnrWrapper::<SecretKey>::decode(&mut &valid_record[..]).unwrap();
|
|
let pubkey = enr.0.public_key().encode();
|
|
|
|
assert_eq!(enr.0.ip4(), Some(Ipv4Addr::new(127, 0, 0, 1)));
|
|
assert_eq!(enr.0.id(), Some(String::from("v4")));
|
|
assert_eq!(enr.0.udp4(), Some(DEFAULT_DISCOVERY_PORT));
|
|
assert_eq!(enr.0.tcp4(), None);
|
|
assert_eq!(enr.0.signature(), &signature[..]);
|
|
assert_eq!(pubkey.to_vec(), expected_pubkey);
|
|
assert!(enr.0.verify());
|
|
|
|
assert_eq!(&alloy_rlp::encode(&enr)[..], &valid_record[..]);
|
|
|
|
// ensure the length is equal
|
|
assert_eq!(enr.length(), valid_record.len());
|
|
}
|
|
|
|
// test vector from the enr library rlp encoding tests
|
|
// <https://github.com/sigp/enr/blob/e59dcb45ea07e423a7091d2a6ede4ad6d8ef2840/src/lib.rs#L1019>
|
|
#[test]
|
|
fn decode_enr_rlp() {
|
|
use enr::secp256k1::SecretKey;
|
|
use std::net::Ipv4Addr;
|
|
|
|
let valid_record = hex!("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f");
|
|
let signature = hex!("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c");
|
|
let expected_pubkey =
|
|
hex!("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138");
|
|
|
|
let mut valid_record_buf = valid_record.as_slice();
|
|
let enr = EnrWrapper::<SecretKey>::decode(&mut valid_record_buf).unwrap();
|
|
let pubkey = enr.0.public_key().encode();
|
|
|
|
// Byte array must be consumed after enr has finished decoding
|
|
assert!(valid_record_buf.is_empty());
|
|
|
|
assert_eq!(enr.0.ip4(), Some(Ipv4Addr::new(127, 0, 0, 1)));
|
|
assert_eq!(enr.0.id(), Some(String::from("v4")));
|
|
assert_eq!(enr.0.udp4(), Some(DEFAULT_DISCOVERY_PORT));
|
|
assert_eq!(enr.0.tcp4(), None);
|
|
assert_eq!(enr.0.signature(), &signature[..]);
|
|
assert_eq!(pubkey.to_vec(), expected_pubkey);
|
|
assert!(enr.0.verify());
|
|
}
|
|
|
|
// test vector from the enr library rlp encoding tests
|
|
// <https://github.com/sigp/enr/blob/e59dcb45ea07e423a7091d2a6ede4ad6d8ef2840/src/lib.rs#LL1206C35-L1206C35>
|
|
#[test]
|
|
fn encode_decode_enr_rlp() {
|
|
use enr::{secp256k1::SecretKey, EnrPublicKey};
|
|
use std::net::Ipv4Addr;
|
|
|
|
let key = SecretKey::new(&mut rand::rngs::OsRng);
|
|
let ip = Ipv4Addr::new(127, 0, 0, 1);
|
|
let tcp = 3000;
|
|
|
|
let enr = {
|
|
let mut builder = Enr::builder();
|
|
builder.ip(ip.into());
|
|
builder.tcp4(tcp);
|
|
EnrWrapper::new(builder.build(&key).unwrap())
|
|
};
|
|
|
|
let mut encoded_bytes = &alloy_rlp::encode(&enr)[..];
|
|
let decoded_enr = EnrWrapper::<SecretKey>::decode(&mut encoded_bytes).unwrap();
|
|
|
|
// Byte array must be consumed after enr has finished decoding
|
|
assert!(encoded_bytes.is_empty());
|
|
|
|
assert_eq!(decoded_enr, enr);
|
|
assert_eq!(decoded_enr.0.id(), Some("v4".into()));
|
|
assert_eq!(decoded_enr.0.ip4(), Some(ip));
|
|
assert_eq!(decoded_enr.0.tcp4(), Some(tcp));
|
|
assert_eq!(decoded_enr.0.public_key().encode(), key.public().encode());
|
|
assert!(decoded_enr.0.verify());
|
|
}
|
|
}
|