From eb5427ef7cc38e8df1f212cee9f0089e44f07d6d Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Tue, 9 Dec 2025 10:31:50 -0300 Subject: [PATCH 01/29] initial discv5 --- crates/networking/p2p/discv5/messages.rs | 244 +++++++++++++++++++++++ crates/networking/p2p/discv5/mod.rs | 1 + crates/networking/p2p/p2p.rs | 1 + 3 files changed, 246 insertions(+) create mode 100644 crates/networking/p2p/discv5/messages.rs create mode 100644 crates/networking/p2p/discv5/mod.rs diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs new file mode 100644 index 00000000000..3233bf84296 --- /dev/null +++ b/crates/networking/p2p/discv5/messages.rs @@ -0,0 +1,244 @@ +use crate::{ + types::{Endpoint, Node, NodeRecord}, + utils::{current_unix_time, node_id}, +}; +use bytes::BufMut; +use ethrex_common::{H256, H512, H520, utils::keccak}; +use ethrex_crypto::keccak::keccak_hash; +use ethrex_rlp::{ + decode::RLPDecode, + encode::RLPEncode, + error::RLPDecodeError, + structs::{self, Decoder, Encoder}, +}; +use secp256k1::{ + SecretKey, + ecdsa::{RecoverableSignature, RecoveryId}, +}; +use std::{convert::Into, io::ErrorKind}; + +#[derive(Debug, thiserror::Error)] +pub enum PacketDecodeErr { + #[error("RLP decoding error")] + RLPDecodeError(#[from] RLPDecodeError), + #[error("Invalid packet size")] + InvalidSize, + #[error("Hash mismatch")] + HashMismatch, + #[error("Invalid signature")] + InvalidSignature, + #[error("Discv4 decoding error: {0}")] + Discv4DecodingError(String), + #[error("Io Error: {0}")] + IoError(#[from] std::io::Error), +} + +impl From for std::io::Error { + fn from(error: PacketDecodeErr) -> Self { + std::io::Error::new(ErrorKind::InvalidData, error.to_string()) + } +} + +#[derive(Debug, Clone)] +pub struct Packet { + hash: H256, + signature: H520, + message: Message, + public_key: H512, +} + +impl Packet { + pub fn decode(encoded_packet: &[u8]) -> Result { + // the packet structure is + // hash || signature || packet-type || packet-data + let hash_len = 32; + let signature_len = 65; + let header_size = hash_len + signature_len; // 97 + + if encoded_packet.len() < header_size + 1 { + return Err(PacketDecodeErr::InvalidSize); + }; + + let hash = H256::from_slice(&encoded_packet[..hash_len]); + let signature_bytes = &encoded_packet[hash_len..header_size]; + let packet_type = encoded_packet[header_size]; + let encoded_msg = &encoded_packet[header_size..]; + + let header_hash = keccak(&encoded_packet[hash_len..]); + + if hash != header_hash { + return Err(PacketDecodeErr::HashMismatch); + } + + let digest: [u8; 32] = keccak_hash(encoded_msg); + + let rid = RecoveryId::try_from(Into::::into(signature_bytes[64])) + .map_err(|_| PacketDecodeErr::InvalidSignature)?; + + let peer_pk = secp256k1::SECP256K1 + .recover_ecdsa( + &secp256k1::Message::from_digest(digest), + &RecoverableSignature::from_compact(&signature_bytes[0..64], rid) + .map_err(|_| PacketDecodeErr::InvalidSignature)?, + ) + .map_err(|_| PacketDecodeErr::InvalidSignature)?; + + let encoded = peer_pk.serialize_uncompressed(); + + let public_key = H512::from_slice(&encoded[1..]); + let signature = H520::from_slice(signature_bytes); + let message = Message::decode_with_type(packet_type, &encoded_msg[1..]) + .map_err(PacketDecodeErr::RLPDecodeError)?; + + Ok(Self { + hash, + signature, + message, + public_key, + }) + } + + pub fn get_hash(&self) -> H256 { + self.hash + } + + pub fn get_message(&self) -> &Message { + &self.message + } + + #[allow(unused)] + pub fn get_signature(&self) -> H520 { + self.signature + } + + pub fn get_public_key(&self) -> H512 { + self.public_key + } + + pub fn get_node_id(&self) -> H256 { + node_id(&self.public_key) + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum Message { + Ping(PingMessage), +} + +impl Message { + pub fn encode_with_header(&self, buf: &mut dyn BufMut, node_signer: &SecretKey) { + let signature_size = 65_usize; + let mut data: Vec = Vec::with_capacity(signature_size.next_power_of_two()); + data.resize(signature_size, 0); + + self.encode_with_type(&mut data); + + let digest: [u8; 32] = keccak_hash(&data[signature_size..]); + + let (recovery_id, signature) = secp256k1::SECP256K1 + .sign_ecdsa_recoverable(&secp256k1::Message::from_digest(digest), node_signer) + .serialize_compact(); + + data[..signature_size - 1].copy_from_slice(&signature); + data[signature_size - 1] = Into::::into(recovery_id) as u8; + + let hash = keccak_hash(&data[..]); + buf.put_slice(&hash); + buf.put_slice(&data[..]); + } + + fn encode_with_type(&self, buf: &mut dyn BufMut) { + buf.put_u8(self.packet_type()); + match self { + Message::Ping(msg) => msg.encode(buf), + } + } + + pub fn decode_with_type(packet_type: u8, msg: &[u8]) -> Result { + // NOTE: extra elements inside the message should be ignored, along with extra data + // after the message. + match packet_type { + 0x01 => { + let (ping, _rest) = PingMessage::decode_unfinished(msg)?; + Ok(Message::Ping(ping)) + } + _ => Err(RLPDecodeError::MalformedData), + } + } + + fn packet_type(&self) -> u8 { + match self { + Message::Ping(_) => 0x01, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PingMessage { + /// The Ping message version. Should be set to 4, but mustn't be enforced. + pub version: u8, + /// The endpoint of the sender. + pub from: Endpoint, + /// The endpoint of the receiver. + pub to: Endpoint, + /// The expiration time of the message. If the message is older than this time, + /// it shouldn't be responded to. + pub expiration: u64, + /// The ENR sequence number of the sender. This field is optional. + pub enr_seq: Option, +} + +impl PingMessage { + pub fn new(from: Endpoint, to: Endpoint, expiration: u64) -> Self { + Self { + version: 4, + from, + to, + expiration, + enr_seq: None, + } + } + + // TODO: remove when used + #[allow(unused)] + pub fn with_enr_seq(self, enr_seq: u64) -> Self { + Self { + enr_seq: Some(enr_seq), + ..self + } + } +} + +impl RLPEncode for PingMessage { + fn encode(&self, buf: &mut dyn BufMut) { + structs::Encoder::new(buf) + .encode_field(&self.version) + .encode_field(&self.from) + .encode_field(&self.to) + .encode_field(&self.expiration) + .encode_optional_field(&self.enr_seq) + .finish(); + } +} + +impl RLPDecode for PingMessage { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (version, decoder): (u8, Decoder) = decoder.decode_field("version")?; + let (from, decoder) = decoder.decode_field("from")?; + let (to, decoder) = decoder.decode_field("to")?; + let (expiration, decoder) = decoder.decode_field("expiration")?; + let (enr_seq, decoder) = decoder.decode_optional_field(); + + let ping = PingMessage { + version, + from, + to, + expiration, + enr_seq, + }; + // NOTE: as per the spec, any additional elements should be ignored. + let remaining = decoder.finish_unchecked(); + Ok((ping, remaining)) + } +} \ No newline at end of file diff --git a/crates/networking/p2p/discv5/mod.rs b/crates/networking/p2p/discv5/mod.rs new file mode 100644 index 00000000000..374230f410a --- /dev/null +++ b/crates/networking/p2p/discv5/mod.rs @@ -0,0 +1 @@ +pub mod messages; \ No newline at end of file diff --git a/crates/networking/p2p/p2p.rs b/crates/networking/p2p/p2p.rs index 43e6b984828..cd9e5d39154 100644 --- a/crates/networking/p2p/p2p.rs +++ b/crates/networking/p2p/p2p.rs @@ -1,4 +1,5 @@ pub mod discv4; +pub mod discv5; pub(crate) mod metrics; pub mod network; pub mod peer_handler; From a61953f571facdaedaba503893f9370581c3c126 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Wed, 10 Dec 2025 13:40:39 -0300 Subject: [PATCH 02/29] discv5 stub modules --- crates/networking/p2p/discv5/messages.rs | 270 ++++++----------------- crates/networking/p2p/discv5/mod.rs | 2 +- 2 files changed, 73 insertions(+), 199 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 3233bf84296..cf7388e6adc 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -1,244 +1,118 @@ -use crate::{ - types::{Endpoint, Node, NodeRecord}, - utils::{current_unix_time, node_id}, -}; +use aes::cipher::KeyIvInit; use bytes::BufMut; -use ethrex_common::{H256, H512, H520, utils::keccak}; -use ethrex_crypto::keccak::keccak_hash; -use ethrex_rlp::{ - decode::RLPDecode, - encode::RLPEncode, - error::RLPDecodeError, - structs::{self, Decoder, Encoder}, -}; -use secp256k1::{ - SecretKey, - ecdsa::{RecoverableSignature, RecoveryId}, -}; -use std::{convert::Into, io::ErrorKind}; +use secp256k1::SecretKey; + +type Aes256Ctr64BE = ctr::Ctr64BE; #[derive(Debug, thiserror::Error)] pub enum PacketDecodeErr { - #[error("RLP decoding error")] - RLPDecodeError(#[from] RLPDecodeError), #[error("Invalid packet size")] InvalidSize, - #[error("Hash mismatch")] - HashMismatch, - #[error("Invalid signature")] - InvalidSignature, - #[error("Discv4 decoding error: {0}")] - Discv4DecodingError(String), - #[error("Io Error: {0}")] - IoError(#[from] std::io::Error), -} - -impl From for std::io::Error { - fn from(error: PacketDecodeErr) -> Self { - std::io::Error::new(ErrorKind::InvalidData, error.to_string()) - } } #[derive(Debug, Clone)] pub struct Packet { - hash: H256, - signature: H520, message: Message, - public_key: H512, } impl Packet { - pub fn decode(encoded_packet: &[u8]) -> Result { + pub fn decode(signer: &SecretKey, encoded_packet: &[u8]) -> Result { // the packet structure is - // hash || signature || packet-type || packet-data - let hash_len = 32; - let signature_len = 65; - let header_size = hash_len + signature_len; // 97 - - if encoded_packet.len() < header_size + 1 { - return Err(PacketDecodeErr::InvalidSize); - }; - - let hash = H256::from_slice(&encoded_packet[..hash_len]); - let signature_bytes = &encoded_packet[hash_len..header_size]; - let packet_type = encoded_packet[header_size]; - let encoded_msg = &encoded_packet[header_size..]; - - let header_hash = keccak(&encoded_packet[hash_len..]); - - if hash != header_hash { - return Err(PacketDecodeErr::HashMismatch); - } - - let digest: [u8; 32] = keccak_hash(encoded_msg); + // masking-iv || masked-header || message - let rid = RecoveryId::try_from(Into::::into(signature_bytes[64])) - .map_err(|_| PacketDecodeErr::InvalidSignature)?; + // 16 bytes for an u128 + let masking_iv = &encoded_packet[..16]; + // 23 bytes for static header + let _static_header = &encoded_packet[16..39]; - let peer_pk = secp256k1::SECP256K1 - .recover_ecdsa( - &secp256k1::Message::from_digest(digest), - &RecoverableSignature::from_compact(&signature_bytes[0..64], rid) - .map_err(|_| PacketDecodeErr::InvalidSignature)?, - ) - .map_err(|_| PacketDecodeErr::InvalidSignature)?; + let public_key = signer.public_key(secp256k1::SECP256K1); - let encoded = peer_pk.serialize_uncompressed(); - - let public_key = H512::from_slice(&encoded[1..]); - let signature = H520::from_slice(signature_bytes); - let message = Message::decode_with_type(packet_type, &encoded_msg[1..]) - .map_err(PacketDecodeErr::RLPDecodeError)?; + // TODO: implement proper decoding + let _cipher = ::new( + public_key.serialize_uncompressed()[..16].into(), + masking_iv.into(), + ); Ok(Self { - hash, - signature, - message, - public_key, + message: Message::Ping(PingMessage { + req_id: 1, + enr_seq: 1, + }), }) } - pub fn get_hash(&self) -> H256 { - self.hash - } - - pub fn get_message(&self) -> &Message { - &self.message - } - - #[allow(unused)] - pub fn get_signature(&self) -> H520 { - self.signature - } - - pub fn get_public_key(&self) -> H512 { - self.public_key - } - - pub fn get_node_id(&self) -> H256 { - node_id(&self.public_key) + pub fn encode(&self, buf: &mut dyn BufMut, signer: &SecretKey) { + self.message.encode(buf, signer); } } #[derive(Debug, Eq, PartialEq, Clone)] pub enum Message { Ping(PingMessage), + // TODO: add the other messages } impl Message { - pub fn encode_with_header(&self, buf: &mut dyn BufMut, node_signer: &SecretKey) { - let signature_size = 65_usize; - let mut data: Vec = Vec::with_capacity(signature_size.next_power_of_two()); - data.resize(signature_size, 0); - - self.encode_with_type(&mut data); - - let digest: [u8; 32] = keccak_hash(&data[signature_size..]); - - let (recovery_id, signature) = secp256k1::SECP256K1 - .sign_ecdsa_recoverable(&secp256k1::Message::from_digest(digest), node_signer) - .serialize_compact(); - - data[..signature_size - 1].copy_from_slice(&signature); - data[signature_size - 1] = Into::::into(recovery_id) as u8; - - let hash = keccak_hash(&data[..]); - buf.put_slice(&hash); - buf.put_slice(&data[..]); - } - - fn encode_with_type(&self, buf: &mut dyn BufMut) { - buf.put_u8(self.packet_type()); - match self { - Message::Ping(msg) => msg.encode(buf), - } - } - - pub fn decode_with_type(packet_type: u8, msg: &[u8]) -> Result { - // NOTE: extra elements inside the message should be ignored, along with extra data - // after the message. - match packet_type { - 0x01 => { - let (ping, _rest) = PingMessage::decode_unfinished(msg)?; - Ok(Message::Ping(ping)) - } - _ => Err(RLPDecodeError::MalformedData), - } - } - - fn packet_type(&self) -> u8 { - match self { - Message::Ping(_) => 0x01, - } + pub fn encode(&self, _buf: &mut dyn BufMut, _signer: &SecretKey) { + //TODO } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PingMessage { - /// The Ping message version. Should be set to 4, but mustn't be enforced. - pub version: u8, - /// The endpoint of the sender. - pub from: Endpoint, - /// The endpoint of the receiver. - pub to: Endpoint, - /// The expiration time of the message. If the message is older than this time, - /// it shouldn't be responded to. - pub expiration: u64, - /// The ENR sequence number of the sender. This field is optional. - pub enr_seq: Option, + /// The request id of the sender. + pub req_id: u64, + /// The ENR sequence number of the sender. + pub enr_seq: u64, } impl PingMessage { - pub fn new(from: Endpoint, to: Endpoint, expiration: u64) -> Self { - Self { - version: 4, - from, - to, - expiration, - enr_seq: None, - } - } - - // TODO: remove when used - #[allow(unused)] - pub fn with_enr_seq(self, enr_seq: u64) -> Self { - Self { - enr_seq: Some(enr_seq), - ..self - } + pub fn new(req_id: u64, enr_seq: u64) -> Self { + Self { req_id, enr_seq } } } -impl RLPEncode for PingMessage { - fn encode(&self, buf: &mut dyn BufMut) { - structs::Encoder::new(buf) - .encode_field(&self.version) - .encode_field(&self.from) - .encode_field(&self.to) - .encode_field(&self.expiration) - .encode_optional_field(&self.enr_seq) - .finish(); +#[cfg(test)] +mod tests { + use crate::discv5::messages::{Message, Packet, PingMessage}; + use hex_literal::hex; + use secp256k1::SecretKey; + + // node-a-key = 0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f + // node-b-key = 0x66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628 + + #[test] + fn test_encode_ping_message() { + // TODO } -} -impl RLPDecode for PingMessage { - fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { - let decoder = Decoder::new(rlp)?; - let (version, decoder): (u8, Decoder) = decoder.decode_field("version")?; - let (from, decoder) = decoder.decode_field("from")?; - let (to, decoder) = decoder.decode_field("to")?; - let (expiration, decoder) = decoder.decode_field("expiration")?; - let (enr_seq, decoder) = decoder.decode_optional_field(); - - let ping = PingMessage { - version, - from, - to, - expiration, - enr_seq, - }; - // NOTE: as per the spec, any additional elements should be ignored. - let remaining = decoder.finish_unchecked(); - Ok((ping, remaining)) + #[test] + fn test_decode_ping_packet() { + // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb + // # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 + // # nonce = 0xffffffffffffffffffffffff + // # read-key = 0x00000000000000000000000000000000 + // # ping.req-id = 0x00000001 + // # ping.enr-seq = 2 + // + // 00000000000000000000000000000000088b3d4342774649325f313964a39e55 + // ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 + // 4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc + let node_b_key = SecretKey::from_slice(&hex!( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" + )) + .unwrap(); + + let encoded = &hex!( + "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" + ); + let decoded = Packet::decode(&node_b_key, encoded).unwrap(); + let message = decoded.message; + let expected = Message::Ping(PingMessage { + req_id: 0x00000001, + enr_seq: 2, + }); + + assert_eq!(message, expected); } -} \ No newline at end of file +} diff --git a/crates/networking/p2p/discv5/mod.rs b/crates/networking/p2p/discv5/mod.rs index 374230f410a..ba63992f3cb 100644 --- a/crates/networking/p2p/discv5/mod.rs +++ b/crates/networking/p2p/discv5/mod.rs @@ -1 +1 @@ -pub mod messages; \ No newline at end of file +pub mod messages; From e1a8a838b54b09678c0fa3689fced801c78ad66d Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Thu, 11 Dec 2025 11:29:00 -0300 Subject: [PATCH 03/29] Ordinary packet --- crates/networking/p2p/discv5/messages.rs | 193 ++++++++++++++++++++--- crates/networking/p2p/rlpx/utils.rs | 8 +- 2 files changed, 168 insertions(+), 33 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index cf7388e6adc..10468022cb3 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -1,48 +1,134 @@ -use aes::cipher::KeyIvInit; +use std::array::TryFromSliceError; + +use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherError}; use bytes::BufMut; +use ethrex_common::H256; +use ethrex_rlp::{decode::RLPDecode, error::RLPDecodeError, structs::Decoder}; use secp256k1::SecretKey; -type Aes256Ctr64BE = ctr::Ctr64BE; +type Aes128Ctr64BE = ctr::Ctr64BE; + +// Max and min packet sizes as defined in +// https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md#udp-communication +// Used for package validation +const MIN_PACKET_SIZE: usize = 63; +const MAX_PACKET_SIZE: usize = 1280; +// protocol id for validation +const PROTOCOL_ID: &[u8] = b"discv5"; +// masking-iv size for a u128 +const IV_MASKING_SIZE: usize = 16; +// static_header end limit: 23 bytes from static_header + 16 from iv_masking +const STATIC_HEADER_END: usize = IV_MASKING_SIZE + 23; #[derive(Debug, thiserror::Error)] pub enum PacketDecodeErr { + #[error("RLP decoding error")] + RLPDecodeError(#[from] RLPDecodeError), #[error("Invalid packet size")] InvalidSize, + #[error("Invalid protocol id: {0}")] + InvalidProtocolId(String), + #[error("Stream Cipher Error: {0}")] + ChipherError(String), + #[error("TryFromSliceError: {0}")] + TryFromSliceError(#[from] TryFromSliceError), +} + +impl From for PacketDecodeErr { + fn from(error: StreamCipherError) -> Self { + PacketDecodeErr::ChipherError(error.to_string()) + } } #[derive(Debug, Clone)] -pub struct Packet { - message: Message, +enum Packet { + Ordinary(Ordinary), + // WhoAreYou(WhoAreYou), + // Handshake(Handshake), } impl Packet { - pub fn decode(signer: &SecretKey, encoded_packet: &[u8]) -> Result { + pub fn decode(dest_id: &H256, encoded_packet: &[u8]) -> Result { + if encoded_packet.len() < MIN_PACKET_SIZE || encoded_packet.len() > MAX_PACKET_SIZE { + return Err(PacketDecodeErr::InvalidSize); + } + // the packet structure is // masking-iv || masked-header || message - // 16 bytes for an u128 - let masking_iv = &encoded_packet[..16]; - // 23 bytes for static header - let _static_header = &encoded_packet[16..39]; + let masking_iv = &encoded_packet[..IV_MASKING_SIZE]; - let public_key = signer.public_key(secp256k1::SECP256K1); + let mut cipher = ::new(dest_id[..16].into(), masking_iv.into()); - // TODO: implement proper decoding - let _cipher = ::new( - public_key.serialize_uncompressed()[..16].into(), - masking_iv.into(), - ); + let (static_header, flag, nonce, authdata, authdata_end) = + Packet::decode_header(&mut cipher, encoded_packet)?; - Ok(Self { - message: Message::Ping(PingMessage { - req_id: 1, - enr_seq: 1, - }), - }) + match flag { + 0x01 => Ok(Packet::Ordinary(Ordinary::decode( + masking_iv, + static_header, + authdata, + nonce, + encoded_packet, + )?)), + _ => Err(RLPDecodeError::MalformedData)?, + } + } + + pub fn decode_header( + cipher: &mut T, + encoded_packet: &[u8], + ) -> Result<(Vec, u8, Vec, Vec, usize), PacketDecodeErr> { + // static header + let mut static_header = encoded_packet[IV_MASKING_SIZE..STATIC_HEADER_END].to_vec(); + + cipher.try_apply_keystream(&mut static_header)?; + + // static-header = protocol-id || version || flag || nonce || authdata-size + + //protocol_id check + let protocol_id = &static_header[..6]; + if protocol_id != PROTOCOL_ID { + return Err(PacketDecodeErr::InvalidProtocolId( + match str::from_utf8(&protocol_id) { + Ok(result) => result.to_string(), + Err(_) => format!("{:?}", protocol_id), + }, + )); + } + + //let version = &static_header[6..8]; + let flag = static_header[8]; + let nonce = static_header[9..21].to_vec(); + let authdata_size = u16::from_be_bytes(static_header[21..23].try_into()?) as usize; + let authdata_end = STATIC_HEADER_END + authdata_size; + let authdata = &mut encoded_packet[STATIC_HEADER_END..authdata_end].to_vec(); + + cipher.try_apply_keystream(authdata)?; + + Ok((static_header, flag, nonce, authdata.to_vec(), authdata_end)) } pub fn encode(&self, buf: &mut dyn BufMut, signer: &SecretKey) { - self.message.encode(buf, signer); + //self.message.encode(buf, signer); + } +} + +#[derive(Debug, Clone)] +struct Ordinary { + message: Message, +} + +impl Ordinary { + pub fn decode( + masking_iv: &[u8], + static_header: Vec, + authdata: Vec, + nonce: Vec, + encoded_packet: &[u8], + ) -> Result { + let message = Message::decode_with_type(1, encoded_packet)?; + Ok(Ordinary { message }) } } @@ -53,6 +139,36 @@ pub enum Message { } impl Message { + pub fn decode_with_type(packet_type: u8, msg: &[u8]) -> Result { + match packet_type { + 0x01 => { + let (ping, _rest) = PingMessage::decode_unfinished(msg)?; + Ok(Message::Ping(ping)) + } + // 0x02 => { + // let (pong, _rest) = PongMessage::decode_unfinished(msg)?; + // Ok(Message::Pong(pong)) + // } + // 0x03 => { + // let (find_node_msg, _rest) = FindNodeMessage::decode_unfinished(msg)?; + // Ok(Message::FindNode(find_node_msg)) + // } + // 0x04 => { + // let (neighbors_msg, _rest) = NeighborsMessage::decode_unfinished(msg)?; + // Ok(Message::Neighbors(neighbors_msg)) + // } + // 0x05 => { + // let (enr_request_msg, _rest) = ENRRequestMessage::decode_unfinished(msg)?; + // Ok(Message::ENRRequest(enr_request_msg)) + // } + // 0x06 => { + // let (enr_response_msg, _rest) = ENRResponseMessage::decode_unfinished(msg)?; + // Ok(Message::ENRResponse(enr_response_msg)) + // } + _ => Err(RLPDecodeError::MalformedData), + } + } + pub fn encode(&self, _buf: &mut dyn BufMut, _signer: &SecretKey) { //TODO } @@ -72,14 +188,38 @@ impl PingMessage { } } +impl RLPDecode for PingMessage { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (req_id, decoder) = decoder.decode_field("req_id")?; + let (enr_seq, decoder) = decoder.decode_field("enr_seq")?; + + let ping = PingMessage { req_id, enr_seq }; + // NOTE: as per the spec, any additional elements should be ignored. + let remaining = decoder.finish_unchecked(); + Ok((ping, remaining)) + } +} + #[cfg(test)] mod tests { - use crate::discv5::messages::{Message, Packet, PingMessage}; + use crate::{ + discv5::messages::{Message, Ordinary, Packet, PingMessage}, + utils::{node_id, public_key_from_signing_key}, + }; use hex_literal::hex; use secp256k1::SecretKey; // node-a-key = 0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f // node-b-key = 0x66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628 + // let node_a_key = SecretKey::from_byte_array(&hex!( + // "eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f" + // )) + // .unwrap(); + // let node_b_key = SecretKey::from_byte_array(&hex!( + // "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" + // )) + // .unwrap(); #[test] fn test_encode_ping_message() { @@ -98,16 +238,17 @@ mod tests { // 00000000000000000000000000000000088b3d4342774649325f313964a39e55 // ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 // 4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc - let node_b_key = SecretKey::from_slice(&hex!( + let node_b_key = SecretKey::from_byte_array(&hex!( "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" )) .unwrap(); + let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); + let encoded = &hex!( "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" ); - let decoded = Packet::decode(&node_b_key, encoded).unwrap(); - let message = decoded.message; + let Packet::Ordinary(Ordinary { message }) = Packet::decode(&dest_id, encoded).unwrap(); let expected = Message::Ping(PingMessage { req_id: 0x00000001, enr_seq: 2, diff --git a/crates/networking/p2p/rlpx/utils.rs b/crates/networking/p2p/rlpx/utils.rs index ad6c0e0baf0..81e8515101e 100644 --- a/crates/networking/p2p/rlpx/utils.rs +++ b/crates/networking/p2p/rlpx/utils.rs @@ -1,5 +1,4 @@ -use ethrex_common::utils::keccak; -use ethrex_common::{H256, H512}; +use ethrex_common::H512; use ethrex_rlp::error::{RLPDecodeError, RLPEncodeError}; use secp256k1::ecdh::shared_secret_point; use secp256k1::{PublicKey, SecretKey}; @@ -45,11 +44,6 @@ pub fn kdf(secret: &[u8], output: &mut [u8]) -> Result<(), CryptographyError> { .map_err(|error| CryptographyError::CouldNotGetKeyFromSecret(error.to_string())) } -/// Cpmputes the node_id from a public key (aka computes the Keccak256 hash of the given public key) -pub fn node_id(public_key: &H512) -> H256 { - keccak(public_key) -} - /// Decompresses the received public key pub fn decompress_pubkey(pk: &PublicKey) -> H512 { let bytes = pk.serialize_uncompressed(); From fec454e0ba6079b9659cc25bd74e4d59b27bb867 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Thu, 11 Dec 2025 12:15:48 -0300 Subject: [PATCH 04/29] Added WhoAreYou packet --- crates/networking/p2p/discv5/messages.rs | 102 +++++++++++++++++++---- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 10468022cb3..8c713a9ccb1 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -40,10 +40,10 @@ impl From for PacketDecodeErr { } } -#[derive(Debug, Clone)] -enum Packet { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Packet { Ordinary(Ordinary), - // WhoAreYou(WhoAreYou), + WhoAreYou(WhoAreYou), // Handshake(Handshake), } @@ -64,12 +64,18 @@ impl Packet { Packet::decode_header(&mut cipher, encoded_packet)?; match flag { - 0x01 => Ok(Packet::Ordinary(Ordinary::decode( + 0x00 => Ok(Packet::Ordinary(Ordinary::decode( + masking_iv, + static_header, + authdata, + nonce, + &encoded_packet[authdata_end..], + )?)), + 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode( masking_iv, static_header, authdata, nonce, - encoded_packet, )?)), _ => Err(RLPDecodeError::MalformedData)?, } @@ -114,8 +120,8 @@ impl Packet { } } -#[derive(Debug, Clone)] -struct Ordinary { +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Ordinary { message: Message, } @@ -125,13 +131,47 @@ impl Ordinary { static_header: Vec, authdata: Vec, nonce: Vec, - encoded_packet: &[u8], + encrypted_message: &[u8], ) -> Result { - let message = Message::decode_with_type(1, encoded_packet)?; + // message = aesgcm_encrypt(initiator-key, nonce, message-pt, message-ad) + // message-pt = message-type || message-data + // message-ad = masking-iv || header + let mut message_ad = masking_iv.to_vec(); + message_ad.extend_from_slice(&static_header); + message_ad.extend_from_slice(&authdata); + + let message = Message::decode_with_type(1, encrypted_message)?; Ok(Ordinary { message }) } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WhoAreYou { + pub id_nonce: Vec, + pub enr_seq: u64, +} + +impl WhoAreYou { + pub fn decode( + masking_iv: &[u8], + static_header: Vec, + authdata: Vec, + nonce: Vec, + ) -> Result { + // message = aesgcm_encrypt(initiator-key, nonce, message-pt, message-ad) + // message-pt = message-type || message-data + // message-ad = masking-iv || header + let mut message_ad = masking_iv.to_vec(); + message_ad.extend_from_slice(&static_header); + message_ad.extend_from_slice(&authdata); + + let id_nonce = vec![]; + let enr_seq = 0; + + Ok(WhoAreYou { id_nonce, enr_seq }) + } +} + #[derive(Debug, Eq, PartialEq, Clone)] pub enum Message { Ping(PingMessage), @@ -204,7 +244,7 @@ impl RLPDecode for PingMessage { #[cfg(test)] mod tests { use crate::{ - discv5::messages::{Message, Ordinary, Packet, PingMessage}, + discv5::messages::{Message, Ordinary, Packet, PingMessage, WhoAreYou}, utils::{node_id, public_key_from_signing_key}, }; use hex_literal::hex; @@ -248,12 +288,44 @@ mod tests { let encoded = &hex!( "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" ); - let Packet::Ordinary(Ordinary { message }) = Packet::decode(&dest_id, encoded).unwrap(); - let expected = Message::Ping(PingMessage { - req_id: 0x00000001, - enr_seq: 2, + let packet = Packet::decode(&dest_id, encoded).unwrap(); + let expected = Packet::Ordinary(Ordinary { + message: Message::Ping(PingMessage { + req_id: 0x00000001, + enr_seq: 2, + }), + }); + + assert_eq!(packet, expected); + } + + #[test] + fn test_decode_whoareyou_packet() { + // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb + // # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 + // # whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000 + // # whoareyou.request-nonce = 0x0102030405060708090a0b0c + // # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 + // # whoareyou.enr-seq = 0 + // + // 00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad + // 1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d + let node_b_key = SecretKey::from_byte_array(&hex!( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" + )) + .unwrap(); + + let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); + + let encoded = &hex!( + "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" + ); + let packet = Packet::decode(&dest_id, encoded).unwrap(); + let expected = Packet::WhoAreYou(WhoAreYou { + id_nonce: (&hex!("0102030405060708090a0b0c0d0e0f10")).to_vec(), + enr_seq: 0, }); - assert_eq!(message, expected); + assert_eq!(packet, expected); } } From 6ecfb65eeb9d0fcb10cf378f50f50d23f215a2f3 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Thu, 11 Dec 2025 12:29:07 -0300 Subject: [PATCH 05/29] WhoAreYou decode test pass --- crates/networking/p2p/discv5/messages.rs | 85 ++++++++++-------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 8c713a9ccb1..2982e121c8a 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -71,12 +71,7 @@ impl Packet { nonce, &encoded_packet[authdata_end..], )?)), - 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode( - masking_iv, - static_header, - authdata, - nonce, - )?)), + 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode(authdata)?)), _ => Err(RLPDecodeError::MalformedData)?, } } @@ -152,21 +147,9 @@ pub struct WhoAreYou { } impl WhoAreYou { - pub fn decode( - masking_iv: &[u8], - static_header: Vec, - authdata: Vec, - nonce: Vec, - ) -> Result { - // message = aesgcm_encrypt(initiator-key, nonce, message-pt, message-ad) - // message-pt = message-type || message-data - // message-ad = masking-iv || header - let mut message_ad = masking_iv.to_vec(); - message_ad.extend_from_slice(&static_header); - message_ad.extend_from_slice(&authdata); - - let id_nonce = vec![]; - let enr_seq = 0; + pub fn decode(authdata: Vec) -> Result { + let id_nonce = authdata[..16].to_vec(); + let enr_seq = u64::from_be_bytes(authdata[16..].try_into()?); Ok(WhoAreYou { id_nonce, enr_seq }) } @@ -262,22 +245,16 @@ mod tests { // .unwrap(); #[test] - fn test_encode_ping_message() { - // TODO - } - - #[test] - fn test_decode_ping_packet() { + fn test_decode_whoareyou_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb // # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 - // # nonce = 0xffffffffffffffffffffffff - // # read-key = 0x00000000000000000000000000000000 - // # ping.req-id = 0x00000001 - // # ping.enr-seq = 2 + // # whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000 + // # whoareyou.request-nonce = 0x0102030405060708090a0b0c + // # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 + // # whoareyou.enr-seq = 0 // - // 00000000000000000000000000000000088b3d4342774649325f313964a39e55 - // ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 - // 4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc + // 00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad + // 1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d let node_b_key = SecretKey::from_byte_array(&hex!( "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" )) @@ -286,30 +263,34 @@ mod tests { let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); let encoded = &hex!( - "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" + "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" ); let packet = Packet::decode(&dest_id, encoded).unwrap(); - let expected = Packet::Ordinary(Ordinary { - message: Message::Ping(PingMessage { - req_id: 0x00000001, - enr_seq: 2, - }), + let expected = Packet::WhoAreYou(WhoAreYou { + id_nonce: (&hex!("0102030405060708090a0b0c0d0e0f10")).to_vec(), + enr_seq: 0, }); assert_eq!(packet, expected); } #[test] - fn test_decode_whoareyou_packet() { + fn test_encode_ping_message() { + // TODO + } + + #[test] + fn test_decode_ping_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb // # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 - // # whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000 - // # whoareyou.request-nonce = 0x0102030405060708090a0b0c - // # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 - // # whoareyou.enr-seq = 0 + // # nonce = 0xffffffffffffffffffffffff + // # read-key = 0x00000000000000000000000000000000 + // # ping.req-id = 0x00000001 + // # ping.enr-seq = 2 // - // 00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad - // 1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d + // 00000000000000000000000000000000088b3d4342774649325f313964a39e55 + // ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 + // 4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc let node_b_key = SecretKey::from_byte_array(&hex!( "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" )) @@ -318,12 +299,14 @@ mod tests { let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); let encoded = &hex!( - "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" + "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" ); let packet = Packet::decode(&dest_id, encoded).unwrap(); - let expected = Packet::WhoAreYou(WhoAreYou { - id_nonce: (&hex!("0102030405060708090a0b0c0d0e0f10")).to_vec(), - enr_seq: 0, + let expected = Packet::Ordinary(Ordinary { + message: Message::Ping(PingMessage { + req_id: 0x00000001, + enr_seq: 2, + }), }); assert_eq!(packet, expected); From 0de7e3916b76229f41509e84ee635691f3610551 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Thu, 11 Dec 2025 15:48:53 -0300 Subject: [PATCH 06/29] WhoAreYou encode test pass --- crates/networking/p2p/discv5/messages.rs | 116 ++++++++++++++++++++--- 1 file changed, 103 insertions(+), 13 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 2982e121c8a..9e08b16e5c9 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -15,6 +15,7 @@ const MIN_PACKET_SIZE: usize = 63; const MAX_PACKET_SIZE: usize = 1280; // protocol id for validation const PROTOCOL_ID: &[u8] = b"discv5"; +const PROTOCOL_VERSION: u16 = 0x0001; // masking-iv size for a u128 const IV_MASKING_SIZE: usize = 16; // static_header end limit: 23 bytes from static_header + 16 from iv_masking @@ -71,12 +72,32 @@ impl Packet { nonce, &encoded_packet[authdata_end..], )?)), - 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode(authdata)?)), + 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode(&authdata)?)), _ => Err(RLPDecodeError::MalformedData)?, } } - pub fn decode_header( + pub fn encode( + &self, + buf: &mut dyn BufMut, + masking_iv: &[u8], + nonce: Vec, + dest_id: &H256, + ) -> Result<(), PacketDecodeErr> { + buf.put_slice(masking_iv); + + let mut cipher = ::new(dest_id[..16].into(), masking_iv.into()); + + match self { + Packet::Ordinary(_ordinary) => todo!(), + Packet::WhoAreYou(who_are_you) => { + who_are_you.encode_header(buf, &mut cipher, nonce)?; + } + } + Ok(()) + } + + fn decode_header( cipher: &mut T, encoded_packet: &[u8], ) -> Result<(Vec, u8, Vec, Vec, usize), PacketDecodeErr> { @@ -86,7 +107,6 @@ impl Packet { cipher.try_apply_keystream(&mut static_header)?; // static-header = protocol-id || version || flag || nonce || authdata-size - //protocol_id check let protocol_id = &static_header[..6]; if protocol_id != PROTOCOL_ID { @@ -106,13 +126,8 @@ impl Packet { let authdata = &mut encoded_packet[STATIC_HEADER_END..authdata_end].to_vec(); cipher.try_apply_keystream(authdata)?; - Ok((static_header, flag, nonce, authdata.to_vec(), authdata_end)) } - - pub fn encode(&self, buf: &mut dyn BufMut, signer: &SecretKey) { - //self.message.encode(buf, signer); - } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -125,7 +140,7 @@ impl Ordinary { masking_iv: &[u8], static_header: Vec, authdata: Vec, - nonce: Vec, + _nonce: Vec, encrypted_message: &[u8], ) -> Result { // message = aesgcm_encrypt(initiator-key, nonce, message-pt, message-ad) @@ -142,13 +157,41 @@ impl Ordinary { #[derive(Debug, Clone, PartialEq, Eq)] pub struct WhoAreYou { - pub id_nonce: Vec, + pub id_nonce: u128, pub enr_seq: u64, } impl WhoAreYou { - pub fn decode(authdata: Vec) -> Result { - let id_nonce = authdata[..16].to_vec(); + fn encode_header( + &self, + buf: &mut dyn BufMut, + cipher: &mut T, + nonce: Vec, + ) -> Result<(), PacketDecodeErr> { + let mut static_header = Vec::new(); + static_header.put_slice(PROTOCOL_ID); + static_header.put_slice(&PROTOCOL_VERSION.to_be_bytes()); + static_header.put_u8(0x01); + static_header.put_slice(&nonce); + static_header.put_slice(&24u16.to_be_bytes()); + cipher.try_apply_keystream(&mut static_header)?; + buf.put_slice(&static_header); + + let mut authdata = Vec::new(); + self.encode(&mut authdata); + cipher.try_apply_keystream(&mut authdata)?; + buf.put_slice(&authdata); + + Ok(()) + } + + fn encode(&self, buf: &mut dyn BufMut) { + buf.put_slice(&self.id_nonce.to_be_bytes()); + buf.put_slice(&self.enr_seq.to_be_bytes()); + } + + pub fn decode(authdata: &Vec) -> Result { + let id_nonce = u128::from_be_bytes(authdata[..16].try_into()?); let enr_seq = u64::from_be_bytes(authdata[16..].try_into()?); Ok(WhoAreYou { id_nonce, enr_seq }) @@ -244,6 +287,48 @@ mod tests { // )) // .unwrap(); + #[test] + fn test_encode_whoareyou_packet() { + // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb + // # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 + // # whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000 + // # whoareyou.request-nonce = 0x0102030405060708090a0b0c + // # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 + // # whoareyou.enr-seq = 0 + // + // 00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad + // 1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d + let node_b_key = SecretKey::from_byte_array(&hex!( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" + )) + .unwrap(); + + let packet = Packet::WhoAreYou(WhoAreYou { + id_nonce: u128::from_be_bytes( + (&hex!("0102030405060708090a0b0c0d0e0f10")) + .to_vec() + .try_into() + .unwrap(), + ), + enr_seq: 0, + }); + + let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); + let mut buf = Vec::new(); + + let _ = packet.encode( + &mut buf, + &hex!("00000000000000000000000000000000"), + hex!("0102030405060708090a0b0c").to_vec(), + &dest_id, + ); + let expected = &hex!( + "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" + ); + + assert_eq!(buf, expected); + } + #[test] fn test_decode_whoareyou_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb @@ -267,7 +352,12 @@ mod tests { ); let packet = Packet::decode(&dest_id, encoded).unwrap(); let expected = Packet::WhoAreYou(WhoAreYou { - id_nonce: (&hex!("0102030405060708090a0b0c0d0e0f10")).to_vec(), + id_nonce: u128::from_be_bytes( + (&hex!("0102030405060708090a0b0c0d0e0f10")) + .to_vec() + .try_into() + .unwrap(), + ), enr_seq: 0, }); From a56f4da611aeb8ca6be6db92f40272ca14ca9482 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Thu, 11 Dec 2025 17:26:57 -0300 Subject: [PATCH 07/29] protocol version check --- crates/networking/p2p/discv5/messages.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 9e08b16e5c9..66cda354447 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -13,7 +13,7 @@ type Aes128Ctr64BE = ctr::Ctr64BE; // Used for package validation const MIN_PACKET_SIZE: usize = 63; const MAX_PACKET_SIZE: usize = 1280; -// protocol id for validation +// protocol data const PROTOCOL_ID: &[u8] = b"discv5"; const PROTOCOL_VERSION: u16 = 0x0001; // masking-iv size for a u128 @@ -27,8 +27,8 @@ pub enum PacketDecodeErr { RLPDecodeError(#[from] RLPDecodeError), #[error("Invalid packet size")] InvalidSize, - #[error("Invalid protocol id: {0}")] - InvalidProtocolId(String), + #[error("Invalid protocol: {0}")] + InvalidProtocol(String), #[error("Stream Cipher Error: {0}")] ChipherError(String), #[error("TryFromSliceError: {0}")] @@ -107,18 +107,18 @@ impl Packet { cipher.try_apply_keystream(&mut static_header)?; // static-header = protocol-id || version || flag || nonce || authdata-size - //protocol_id check + //protocol check let protocol_id = &static_header[..6]; - if protocol_id != PROTOCOL_ID { - return Err(PacketDecodeErr::InvalidProtocolId( + let version = u16::from_be_bytes(static_header[6..8].try_into()?); + if protocol_id != PROTOCOL_ID || version != PROTOCOL_VERSION { + return Err(PacketDecodeErr::InvalidProtocol( match str::from_utf8(&protocol_id) { - Ok(result) => result.to_string(), - Err(_) => format!("{:?}", protocol_id), + Ok(result) => format!("{} v{}", result, version), + Err(_) => format!("{:?} v{}", protocol_id, version), }, )); } - //let version = &static_header[6..8]; let flag = static_header[8]; let nonce = static_header[9..21].to_vec(); let authdata_size = u16::from_be_bytes(static_header[21..23].try_into()?) as usize; From 7a8541bfb89117828b0286f1a107a34a2e0223b8 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Fri, 12 Dec 2025 12:46:14 -0300 Subject: [PATCH 08/29] Added Discv5Codec --- crates/networking/p2p/discv5/codec.rs | 51 ++++++++++++++++++++++++ crates/networking/p2p/discv5/messages.rs | 37 +++++++++++------ crates/networking/p2p/discv5/mod.rs | 1 + 3 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 crates/networking/p2p/discv5/codec.rs diff --git a/crates/networking/p2p/discv5/codec.rs b/crates/networking/p2p/discv5/codec.rs new file mode 100644 index 00000000000..143a649e06a --- /dev/null +++ b/crates/networking/p2p/discv5/codec.rs @@ -0,0 +1,51 @@ +use crate::discv5::messages::{Packet, PacketDecodeErr}; + +use bytes::BytesMut; +use ethrex_common::H256; +use tokio_util::codec::{Decoder, Encoder}; + +#[derive(Debug)] +pub struct Discv5Codec { + dest_id: H256, + nonce: u128, +} + +impl Discv5Codec { + pub fn new(dest_id: H256) -> Self { + Self { + dest_id, + nonce: rand::random(), + } + } + + fn new_nonce(&mut self) -> Vec { + self.nonce = self.nonce.wrapping_add(1); + self.nonce.to_be_bytes()[4..].to_vec() + } +} + +impl Decoder for Discv5Codec { + type Item = Packet; + type Error = PacketDecodeErr; + + fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { + if !buf.is_empty() { + Ok(Some(Packet::decode( + &self.dest_id, + &buf.split_to(buf.len()), + )?)) + } else { + Ok(None) + } + } +} + +impl Encoder for Discv5Codec { + type Error = PacketDecodeErr; + + fn encode(&mut self, package: Packet, buf: &mut BytesMut) -> Result<(), Self::Error> { + let masking_iv: u128 = rand::random(); + let nonce = self.new_nonce(); + package.encode(buf, masking_iv, nonce, &self.dest_id) + } +} diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 66cda354447..c52b24830aa 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -33,6 +33,8 @@ pub enum PacketDecodeErr { ChipherError(String), #[error("TryFromSliceError: {0}")] TryFromSliceError(#[from] TryFromSliceError), + #[error("Io Error: {0}")] + IoError(#[from] std::io::Error), } impl From for PacketDecodeErr { @@ -80,13 +82,15 @@ impl Packet { pub fn encode( &self, buf: &mut dyn BufMut, - masking_iv: &[u8], + masking_iv: u128, nonce: Vec, dest_id: &H256, ) -> Result<(), PacketDecodeErr> { - buf.put_slice(masking_iv); + let masking_as_bytes = masking_iv.to_be_bytes(); + buf.put_slice(&masking_as_bytes); - let mut cipher = ::new(dest_id[..16].into(), masking_iv.into()); + let mut cipher = + ::new(dest_id[..16].into(), masking_as_bytes[..].into()); match self { Packet::Ordinary(_ordinary) => todo!(), @@ -205,10 +209,13 @@ pub enum Message { } impl Message { - pub fn decode_with_type(packet_type: u8, msg: &[u8]) -> Result { + pub fn decode_with_type( + packet_type: u8, + encrypted_message: &[u8], + ) -> Result { match packet_type { 0x01 => { - let (ping, _rest) = PingMessage::decode_unfinished(msg)?; + let (ping, _rest) = PingMessage::decode_unfinished(encrypted_message)?; Ok(Message::Ping(ping)) } // 0x02 => { @@ -270,11 +277,16 @@ impl RLPDecode for PingMessage { #[cfg(test)] mod tests { use crate::{ - discv5::messages::{Message, Ordinary, Packet, PingMessage, WhoAreYou}, + discv5::{ + codec::Discv5Codec, + messages::{Message, Ordinary, Packet, PingMessage, WhoAreYou}, + }, utils::{node_id, public_key_from_signing_key}, }; + use bytes::BytesMut; use hex_literal::hex; use secp256k1::SecretKey; + use tokio_util::codec::Decoder as _; // node-a-key = 0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f // node-b-key = 0x66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628 @@ -318,7 +330,7 @@ mod tests { let _ = packet.encode( &mut buf, - &hex!("00000000000000000000000000000000"), + 0, hex!("0102030405060708090a0b0c").to_vec(), &dest_id, ); @@ -346,12 +358,13 @@ mod tests { .unwrap(); let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); + let mut codec = Discv5Codec::new(dest_id); - let encoded = &hex!( + let mut encoded = BytesMut::from(hex!( "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" - ); - let packet = Packet::decode(&dest_id, encoded).unwrap(); - let expected = Packet::WhoAreYou(WhoAreYou { + ).as_slice()); + let packet = codec.decode(&mut encoded).unwrap(); + let expected = Some(Packet::WhoAreYou(WhoAreYou { id_nonce: u128::from_be_bytes( (&hex!("0102030405060708090a0b0c0d0e0f10")) .to_vec() @@ -359,7 +372,7 @@ mod tests { .unwrap(), ), enr_seq: 0, - }); + })); assert_eq!(packet, expected); } diff --git a/crates/networking/p2p/discv5/mod.rs b/crates/networking/p2p/discv5/mod.rs index ba63992f3cb..e623d729da5 100644 --- a/crates/networking/p2p/discv5/mod.rs +++ b/crates/networking/p2p/discv5/mod.rs @@ -1 +1,2 @@ +pub mod codec; pub mod messages; From 334044ee7f44a569f4ddb5fd6abb03b31b5d6cf0 Mon Sep 17 00:00:00 2001 From: MrAzteca Date: Fri, 12 Dec 2025 16:51:14 +0100 Subject: [PATCH 09/29] feat(l1): implement `discv5`'s `Pong` message (#5616) **Motivation** **Description** **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. Closes #5574 and #5575. Co-authored-by: Esteban Dimitroff Hodi --- crates/networking/p2p/discv5/messages.rs | 73 +++++++++++++++++++++--- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index c52b24830aa..e3b7dc07e3d 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -1,9 +1,14 @@ -use std::array::TryFromSliceError; +use std::{array::TryFromSliceError, net::IpAddr}; use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherError}; use bytes::BufMut; use ethrex_common::H256; -use ethrex_rlp::{decode::RLPDecode, error::RLPDecodeError, structs::Decoder}; +use ethrex_rlp::{ + decode::RLPDecode, + encode::RLPEncode, + error::RLPDecodeError, + structs::{Decoder, Encoder}, +}; use secp256k1::SecretKey; type Aes128Ctr64BE = ctr::Ctr64BE; @@ -205,6 +210,7 @@ impl WhoAreYou { #[derive(Debug, Eq, PartialEq, Clone)] pub enum Message { Ping(PingMessage), + Pong(PongMessage), // TODO: add the other messages } @@ -218,10 +224,10 @@ impl Message { let (ping, _rest) = PingMessage::decode_unfinished(encrypted_message)?; Ok(Message::Ping(ping)) } - // 0x02 => { - // let (pong, _rest) = PongMessage::decode_unfinished(msg)?; - // Ok(Message::Pong(pong)) - // } + 0x02 => { + let (pong, _rest) = PongMessage::decode_unfinished(encrypted_message)?; + Ok(Message::Pong(pong)) + } // 0x03 => { // let (find_node_msg, _rest) = FindNodeMessage::decode_unfinished(msg)?; // Ok(Message::FindNode(find_node_msg)) @@ -274,8 +280,47 @@ impl RLPDecode for PingMessage { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PongMessage { + pub req_id: u64, + pub enr_seq: u64, + pub recipient_addr: IpAddr, +} + +impl RLPEncode for PongMessage { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.req_id) + .encode_field(&self.enr_seq) + .encode_field(&self.recipient_addr) + .finish(); + } +} + +impl RLPDecode for PongMessage { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (req_id, decoder) = decoder.decode_field("req_id")?; + let (enr_seq, decoder) = decoder.decode_field("enr_seq")?; + let (recipient_addr, decoder) = decoder.decode_field("recipient_addr")?; + + Ok(( + Self { + req_id, + enr_seq, + recipient_addr, + }, + decoder.finish()?, + )) + } +} + #[cfg(test)] mod tests { + use super::*; + use hex_literal::hex; + use secp256k1::SecretKey; + use std::net::Ipv4Addr; use crate::{ discv5::{ codec::Discv5Codec, @@ -284,8 +329,6 @@ mod tests { utils::{node_id, public_key_from_signing_key}, }; use bytes::BytesMut; - use hex_literal::hex; - use secp256k1::SecretKey; use tokio_util::codec::Decoder as _; // node-a-key = 0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f @@ -414,4 +457,18 @@ mod tests { assert_eq!(packet, expected); } + + // TODO: Test encode pong packet (with known good encoding). + // TODO: Test decode pong packet (from known good encoding). + #[test] + fn pong_packet_codec_roundtrip() { + let pkt = PongMessage { + req_id: 1234, + enr_seq: 4321, + recipient_addr: Ipv4Addr::BROADCAST.into(), + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(PongMessage::decode(&buf).unwrap(), pkt); + } } From 31b82d2ff1057a02ee68b460323de6e49b520fe7 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Fri, 12 Dec 2025 17:31:04 -0300 Subject: [PATCH 10/29] Added decryption and corrected Ping decoding --- Cargo.lock | 64 ++++++++++++++++++ crates/networking/p2p/Cargo.toml | 1 + crates/networking/p2p/discv5/codec.rs | 3 + crates/networking/p2p/discv5/messages.rs | 82 +++++++++++++++++------- 4 files changed, 127 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4966001b69..d80bc41be1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,6 +102,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array 0.14.7", +] + [[package]] name = "aes" version = "0.8.4" @@ -113,6 +123,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -2325,6 +2349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", + "rand_core 0.6.4", "typenum", ] @@ -3881,6 +3906,7 @@ name = "ethrex-p2p" version = "7.0.0" dependencies = [ "aes", + "aes-gcm", "async-trait", "bytes", "concat-kdf", @@ -4644,6 +4670,16 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -7265,6 +7301,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open-fastrlp" version = "0.1.4" @@ -9501,6 +9543,18 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -13766,6 +13820,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unroll" version = "0.1.5" diff --git a/crates/networking/p2p/Cargo.toml b/crates/networking/p2p/Cargo.toml index 3c565d4f214..72b1ce5d9a6 100644 --- a/crates/networking/p2p/Cargo.toml +++ b/crates/networking/p2p/Cargo.toml @@ -46,6 +46,7 @@ serde_json = "1.0.117" concat-kdf = "0.1.0" hmac = "0.12.1" aes = "0.8.4" +aes-gcm = "0.10.3" ctr = "0.9.2" rand = "0.8.5" diff --git a/crates/networking/p2p/discv5/codec.rs b/crates/networking/p2p/discv5/codec.rs index 143a649e06a..d39510bc02d 100644 --- a/crates/networking/p2p/discv5/codec.rs +++ b/crates/networking/p2p/discv5/codec.rs @@ -8,6 +8,7 @@ use tokio_util::codec::{Decoder, Encoder}; pub struct Discv5Codec { dest_id: H256, nonce: u128, + key: Vec, } impl Discv5Codec { @@ -15,6 +16,7 @@ impl Discv5Codec { Self { dest_id, nonce: rand::random(), + key: vec![], } } @@ -32,6 +34,7 @@ impl Decoder for Discv5Codec { if !buf.is_empty() { Ok(Some(Packet::decode( &self.dest_id, + &self.key, &buf.split_to(buf.len()), )?)) } else { diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index e3b7dc07e3d..dd118ba07f0 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -1,7 +1,8 @@ use std::{array::TryFromSliceError, net::IpAddr}; use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherError}; -use bytes::BufMut; +use aes_gcm::{Aes128Gcm, KeyInit, aead::AeadMutInPlace}; +use bytes::{BufMut, Bytes}; use ethrex_common::H256; use ethrex_rlp::{ decode::RLPDecode, @@ -56,7 +57,7 @@ pub enum Packet { } impl Packet { - pub fn decode(dest_id: &H256, encoded_packet: &[u8]) -> Result { + pub fn decode(dest_id: &H256, decrypt_key: &Vec, encoded_packet: &[u8]) -> Result { if encoded_packet.len() < MIN_PACKET_SIZE || encoded_packet.len() > MAX_PACKET_SIZE { return Err(PacketDecodeErr::InvalidSize); } @@ -77,6 +78,7 @@ impl Packet { static_header, authdata, nonce, + decrypt_key, &encoded_packet[authdata_end..], )?)), 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode(&authdata)?)), @@ -149,7 +151,8 @@ impl Ordinary { masking_iv: &[u8], static_header: Vec, authdata: Vec, - _nonce: Vec, + nonce: Vec, + decrypt_key: &Vec, encrypted_message: &[u8], ) -> Result { // message = aesgcm_encrypt(initiator-key, nonce, message-pt, message-ad) @@ -159,9 +162,25 @@ impl Ordinary { message_ad.extend_from_slice(&static_header); message_ad.extend_from_slice(&authdata); - let message = Message::decode_with_type(1, encrypted_message)?; + let mut message = (&encrypted_message).to_vec(); + Self::decrypt(decrypt_key, nonce, &mut message, message_ad)?; + + let message = Message::decode(&message)?; Ok(Ordinary { message }) } + + fn decrypt( + key: &Vec, + nonce: Vec, + message: &mut Vec, + message_ad: Vec, + ) -> Result<(), PacketDecodeErr> { + let mut cipher = Aes128Gcm::new(key[..16].into()); + cipher + .decrypt_in_place(nonce.as_slice().into(), &message_ad, message) + .map_err(|e| PacketDecodeErr::ChipherError(e.to_string()))?; + Ok(()) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -215,17 +234,17 @@ pub enum Message { } impl Message { - pub fn decode_with_type( - packet_type: u8, + pub fn decode( encrypted_message: &[u8], ) -> Result { - match packet_type { + let message_type = encrypted_message[0]; + match message_type { 0x01 => { - let (ping, _rest) = PingMessage::decode_unfinished(encrypted_message)?; + let ping = PingMessage::decode(&encrypted_message[1..])?; Ok(Message::Ping(ping)) } 0x02 => { - let (pong, _rest) = PongMessage::decode_unfinished(encrypted_message)?; + let pong = PongMessage::decode(&encrypted_message[1..])?; Ok(Message::Pong(pong)) } // 0x03 => { @@ -256,26 +275,30 @@ impl Message { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PingMessage { /// The request id of the sender. - pub req_id: u64, + pub req_id: Vec, /// The ENR sequence number of the sender. pub enr_seq: u64, } impl PingMessage { - pub fn new(req_id: u64, enr_seq: u64) -> Self { + pub fn new(req_id: Vec, enr_seq: u64) -> Self { Self { req_id, enr_seq } } } +impl RLPEncode for PingMessage { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.req_id) + .encode_field(&self.enr_seq) + .finish(); + } +} + impl RLPDecode for PingMessage { fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { - let decoder = Decoder::new(rlp)?; - let (req_id, decoder) = decoder.decode_field("req_id")?; - let (enr_seq, decoder) = decoder.decode_field("enr_seq")?; - - let ping = PingMessage { req_id, enr_seq }; - // NOTE: as per the spec, any additional elements should be ignored. - let remaining = decoder.finish_unchecked(); + let ((req_id, enr_seq), remaining): ((Bytes, u64), &[u8]) = RLPDecode::decode_unfinished(rlp)?; + let ping = PingMessage { req_id: req_id.to_vec(), enr_seq }; Ok((ping, remaining)) } } @@ -318,9 +341,6 @@ impl RLPDecode for PongMessage { #[cfg(test)] mod tests { use super::*; - use hex_literal::hex; - use secp256k1::SecretKey; - use std::net::Ipv4Addr; use crate::{ discv5::{ codec::Discv5Codec, @@ -329,6 +349,9 @@ mod tests { utils::{node_id, public_key_from_signing_key}, }; use bytes::BytesMut; + use hex_literal::hex; + use secp256k1::SecretKey; + use std::net::Ipv4Addr; use tokio_util::codec::Decoder as _; // node-a-key = 0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f @@ -447,10 +470,12 @@ mod tests { let encoded = &hex!( "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" ); - let packet = Packet::decode(&dest_id, encoded).unwrap(); + // # read-key = 0x00000000000000000000000000000000 + let read_key = [0;16].to_vec(); + let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); let expected = Packet::Ordinary(Ordinary { message: Message::Ping(PingMessage { - req_id: 0x00000001, + req_id: hex!("00000001").to_vec(), enr_seq: 2, }), }); @@ -458,6 +483,17 @@ mod tests { assert_eq!(packet, expected); } + #[test] + fn ping_packet_codec_roundtrip() { + let pkt = PingMessage { + req_id: [1,2,3,4].to_vec(), + enr_seq: 4321, + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(PingMessage::decode(&buf).unwrap(), pkt); + } + // TODO: Test encode pong packet (with known good encoding). // TODO: Test decode pong packet (from known good encoding). #[test] From db0a79c0c47db6a53a717c83fc834b7834b35889 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Fri, 12 Dec 2025 18:00:36 -0300 Subject: [PATCH 11/29] Corrected Ping encoding --- crates/networking/p2p/discv5/messages.rs | 26 +++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index dd118ba07f0..09831e80915 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -57,7 +57,11 @@ pub enum Packet { } impl Packet { - pub fn decode(dest_id: &H256, decrypt_key: &Vec, encoded_packet: &[u8]) -> Result { + pub fn decode( + dest_id: &H256, + decrypt_key: &Vec, + encoded_packet: &[u8], + ) -> Result { if encoded_packet.len() < MIN_PACKET_SIZE || encoded_packet.len() > MAX_PACKET_SIZE { return Err(PacketDecodeErr::InvalidSize); } @@ -164,7 +168,7 @@ impl Ordinary { let mut message = (&encrypted_message).to_vec(); Self::decrypt(decrypt_key, nonce, &mut message, message_ad)?; - + let message = Message::decode(&message)?; Ok(Ordinary { message }) } @@ -234,9 +238,7 @@ pub enum Message { } impl Message { - pub fn decode( - encrypted_message: &[u8], - ) -> Result { + pub fn decode(encrypted_message: &[u8]) -> Result { let message_type = encrypted_message[0]; match message_type { 0x01 => { @@ -289,7 +291,7 @@ impl PingMessage { impl RLPEncode for PingMessage { fn encode(&self, buf: &mut dyn BufMut) { Encoder::new(buf) - .encode_field(&self.req_id) + .encode_field(&Bytes::from(self.req_id.clone())) .encode_field(&self.enr_seq) .finish(); } @@ -297,8 +299,12 @@ impl RLPEncode for PingMessage { impl RLPDecode for PingMessage { fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { - let ((req_id, enr_seq), remaining): ((Bytes, u64), &[u8]) = RLPDecode::decode_unfinished(rlp)?; - let ping = PingMessage { req_id: req_id.to_vec(), enr_seq }; + let ((req_id, enr_seq), remaining): ((Bytes, u64), &[u8]) = + RLPDecode::decode_unfinished(rlp)?; + let ping = PingMessage { + req_id: req_id.to_vec(), + enr_seq, + }; Ok((ping, remaining)) } } @@ -471,7 +477,7 @@ mod tests { "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" ); // # read-key = 0x00000000000000000000000000000000 - let read_key = [0;16].to_vec(); + let read_key = [0; 16].to_vec(); let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); let expected = Packet::Ordinary(Ordinary { message: Message::Ping(PingMessage { @@ -486,7 +492,7 @@ mod tests { #[test] fn ping_packet_codec_roundtrip() { let pkt = PingMessage { - req_id: [1,2,3,4].to_vec(), + req_id: [1, 2, 3, 4].to_vec(), enr_seq: 4321, }; From 3020428b1d9705bd84a60d4f79370d2d378f5fde Mon Sep 17 00:00:00 2001 From: Edgar Date: Mon, 15 Dec 2025 13:26:22 +0100 Subject: [PATCH 12/29] feat(l1): implement discv5 TalkReq message coding (#5631) **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. Closes #5580 and #5581 --- crates/networking/p2p/discv5/messages.rs | 56 ++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 09831e80915..76927bd02b2 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -234,6 +234,7 @@ impl WhoAreYou { pub enum Message { Ping(PingMessage), Pong(PongMessage), + TalkReq(TalkReqMessage), // TODO: add the other messages } @@ -257,10 +258,10 @@ impl Message { // let (neighbors_msg, _rest) = NeighborsMessage::decode_unfinished(msg)?; // Ok(Message::Neighbors(neighbors_msg)) // } - // 0x05 => { - // let (enr_request_msg, _rest) = ENRRequestMessage::decode_unfinished(msg)?; - // Ok(Message::ENRRequest(enr_request_msg)) - // } + 0x05 => { + let talk_req_msg = TalkReqMessage::decode(&encrypted_message[1..])?; + Ok(Message::TalkReq(talk_req_msg)) + } // 0x06 => { // let (enr_response_msg, _rest) = ENRResponseMessage::decode_unfinished(msg)?; // Ok(Message::ENRResponse(enr_response_msg)) @@ -344,6 +345,41 @@ impl RLPDecode for PongMessage { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TalkReqMessage { + pub req_id: u64, + pub protocol: Bytes, + pub request: Bytes, +} + +impl RLPEncode for TalkReqMessage { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.req_id) + .encode_field(&self.protocol) + .encode_field(&self.request) + .finish(); + } +} + +impl RLPDecode for TalkReqMessage { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (req_id, decoder) = decoder.decode_field("req_id")?; + let (protocol, decoder) = decoder.decode_field("protocol")?; + let (request, decoder) = decoder.decode_field("request")?; + + Ok(( + Self { + req_id, + protocol, + request, + }, + decoder.finish()?, + )) + } +} + #[cfg(test)] mod tests { use super::*; @@ -513,4 +549,16 @@ mod tests { let buf = pkt.encode_to_vec(); assert_eq!(PongMessage::decode(&buf).unwrap(), pkt); } + + #[test] + fn talkreq_packet_codec_roundtrip() { + let pkt = TalkReqMessage { + req_id: 1234, + protocol: Bytes::from_static(&[1, 2, 3, 4]), + request: Bytes::from_static(&[1, 2, 3, 4]), + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(TalkReqMessage::decode(&buf).unwrap(), pkt); + } } From 552160a6f38c1f11f0f57e57d7b9205eade7a9e9 Mon Sep 17 00:00:00 2001 From: Edgar Date: Mon, 15 Dec 2025 13:52:12 +0100 Subject: [PATCH 13/29] feat(l1): implement discv5 nodes message coding (#5630) **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. Closes #5578 and #5579 --- crates/networking/p2p/discv5/messages.rs | 71 ++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 76927bd02b2..c5011cd9851 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -12,6 +12,8 @@ use ethrex_rlp::{ }; use secp256k1::SecretKey; +use crate::types::NodeRecord; + type Aes128Ctr64BE = ctr::Ctr64BE; // Max and min packet sizes as defined in @@ -234,6 +236,7 @@ impl WhoAreYou { pub enum Message { Ping(PingMessage), Pong(PongMessage), + Nodes(NodesMessage), TalkReq(TalkReqMessage), // TODO: add the other messages } @@ -254,10 +257,10 @@ impl Message { // let (find_node_msg, _rest) = FindNodeMessage::decode_unfinished(msg)?; // Ok(Message::FindNode(find_node_msg)) // } - // 0x04 => { - // let (neighbors_msg, _rest) = NeighborsMessage::decode_unfinished(msg)?; - // Ok(Message::Neighbors(neighbors_msg)) - // } + 0x04 => { + let nodes_msg = NodesMessage::decode(&encrypted_message[1..])?; + Ok(Message::Nodes(nodes_msg)) + } 0x05 => { let talk_req_msg = TalkReqMessage::decode(&encrypted_message[1..])?; Ok(Message::TalkReq(talk_req_msg)) @@ -345,6 +348,41 @@ impl RLPDecode for PongMessage { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NodesMessage { + pub req_id: u64, + pub total: u64, + pub nodes: Vec, +} + +impl RLPEncode for NodesMessage { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.req_id) + .encode_field(&self.total) + .encode_field(&self.nodes) + .finish(); + } +} + +impl RLPDecode for NodesMessage { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (req_id, decoder) = decoder.decode_field("req_id")?; + let (total, decoder) = decoder.decode_field("total")?; + let (nodes, decoder) = decoder.decode_field("nodes")?; + + Ok(( + Self { + req_id, + total, + nodes, + }, + decoder.finish()?, + )) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TalkReqMessage { pub req_id: u64, @@ -388,9 +426,11 @@ mod tests { codec::Discv5Codec, messages::{Message, Ordinary, Packet, PingMessage, WhoAreYou}, }, + types::NodeRecordPairs, utils::{node_id, public_key_from_signing_key}, }; use bytes::BytesMut; + use ethrex_common::H512; use hex_literal::hex; use secp256k1::SecretKey; use std::net::Ipv4Addr; @@ -550,6 +590,29 @@ mod tests { assert_eq!(PongMessage::decode(&buf).unwrap(), pkt); } + #[test] + fn nodes_packet_codec_roundtrip() { + let pairs: Vec<(Bytes, Bytes)> = NodeRecordPairs { + id: Some("id".to_string()), + ..Default::default() + } + .try_into() + .unwrap(); + + let pkt = NodesMessage { + req_id: 1234, + total: 2, + nodes: vec![NodeRecord { + seq: 4321, + pairs: pairs, + signature: H512::random(), + }], + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(NodesMessage::decode(&buf).unwrap(), pkt); + } + #[test] fn talkreq_packet_codec_roundtrip() { let pkt = TalkReqMessage { From d2da66cc506a292ee809e17bb6369b0fcb7fa78f Mon Sep 17 00:00:00 2001 From: Edgar Date: Mon, 15 Dec 2025 13:54:33 +0100 Subject: [PATCH 14/29] feat(l1): implement discv5's FindNode message (#5629) **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. Closes #5576 and closes #5577 --- crates/common/types/transaction.rs | 3 +- crates/networking/p2p/discv5/messages.rs | 45 +++++++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index 4352412dbc7..82dd0755759 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -3,7 +3,6 @@ use std::{cmp::min, fmt::Display}; use crate::{errors::EcdsaError, utils::keccak}; use bytes::Bytes; use ethereum_types::{Address, H256, Signature, U256}; -use ethrex_crypto::keccak::keccak_hash; pub use mempool::MempoolTransaction; use rkyv::{Archive, Deserialize as RDeserialize, Serialize as RSerialize}; use serde::{Serialize, ser::SerializeStruct}; @@ -1431,7 +1430,7 @@ pub fn recover_address(signature: Signature, payload: H256) -> Result { - // let (find_node_msg, _rest) = FindNodeMessage::decode_unfinished(msg)?; - // Ok(Message::FindNode(find_node_msg)) - // } + 0x03 => { + let find_node_msg = FindNodeMessage::decode(&encrypted_message[1..])?; + Ok(Message::FindNode(find_node_msg)) + } 0x04 => { let nodes_msg = NodesMessage::decode(&encrypted_message[1..])?; Ok(Message::Nodes(nodes_msg)) @@ -348,6 +349,31 @@ impl RLPDecode for PongMessage { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FindNodeMessage { + pub req_id: u64, + pub distance: Vec, +} + +impl RLPEncode for FindNodeMessage { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.req_id) + .encode_field(&self.distance) + .finish(); + } +} + +impl RLPDecode for FindNodeMessage { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (req_id, decoder) = decoder.decode_field("req_id")?; + let (distance, decoder) = decoder.decode_field("distance")?; + + Ok((Self { req_id, distance }, decoder.finish()?)) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct NodesMessage { pub req_id: u64, @@ -590,6 +616,17 @@ mod tests { assert_eq!(PongMessage::decode(&buf).unwrap(), pkt); } + #[test] + fn findnode_packet_codec_roundtrip() { + let pkt = FindNodeMessage { + req_id: 1234, + distance: vec![1, 2, 3, 4], + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(FindNodeMessage::decode(&buf).unwrap(), pkt); + } + #[test] fn nodes_packet_codec_roundtrip() { let pairs: Vec<(Bytes, Bytes)> = NodeRecordPairs { From 4af6e40c3d560da55f9ab1bb34a91c7de6a829f1 Mon Sep 17 00:00:00 2001 From: Edgar Date: Mon, 15 Dec 2025 14:39:13 +0100 Subject: [PATCH 15/29] chore(l1): fix discv5 branch lints (#5633) **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. Closes #issue_number --- crates/l2/tee/quote-gen/Cargo.lock | 66 +++++++++++++++++++++++- crates/networking/p2p/discv5/messages.rs | 59 +++++++++++++-------- 2 files changed, 103 insertions(+), 22 deletions(-) diff --git a/crates/l2/tee/quote-gen/Cargo.lock b/crates/l2/tee/quote-gen/Cargo.lock index 85aff1548ae..38889b36d21 100644 --- a/crates/l2/tee/quote-gen/Cargo.lock +++ b/crates/l2/tee/quote-gen/Cargo.lock @@ -38,6 +38,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -49,6 +59,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -1301,6 +1325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -2321,6 +2346,7 @@ name = "ethrex-p2p" version = "7.0.0" dependencies = [ "aes", + "aes-gcm", "async-trait", "bytes", "concat-kdf", @@ -2848,6 +2874,16 @@ dependencies = [ "wasi 0.14.4+wasi-0.2.4", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -3491,7 +3527,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde 1.0.228", "serde_core", ] @@ -4183,6 +4219,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open-fastrlp" version = "0.1.4" @@ -4637,6 +4679,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.3", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -6931,6 +6985,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 23e4c769fb2..602fb7c1799 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -58,10 +58,20 @@ pub enum Packet { // Handshake(Handshake), } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PacketHeader { + pub static_header: Vec, + pub flag: u8, + pub nonce: Vec, + pub authdata: Vec, + /// Offset in the encoded packet where authdata ends, i.e where the header ends. + pub header_end_offset: usize, +} + impl Packet { pub fn decode( dest_id: &H256, - decrypt_key: &Vec, + decrypt_key: &[u8], encoded_packet: &[u8], ) -> Result { if encoded_packet.len() < MIN_PACKET_SIZE || encoded_packet.len() > MAX_PACKET_SIZE { @@ -75,19 +85,20 @@ impl Packet { let mut cipher = ::new(dest_id[..16].into(), masking_iv.into()); - let (static_header, flag, nonce, authdata, authdata_end) = - Packet::decode_header(&mut cipher, encoded_packet)?; + let packet_header = Packet::decode_header(&mut cipher, encoded_packet)?; - match flag { + match packet_header.flag { 0x00 => Ok(Packet::Ordinary(Ordinary::decode( masking_iv, - static_header, - authdata, - nonce, + packet_header.static_header, + packet_header.authdata, + packet_header.nonce, decrypt_key, - &encoded_packet[authdata_end..], + &encoded_packet[packet_header.header_end_offset..], + )?)), + 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode( + &packet_header.authdata, )?)), - 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode(&authdata)?)), _ => Err(RLPDecodeError::MalformedData)?, } } @@ -117,7 +128,7 @@ impl Packet { fn decode_header( cipher: &mut T, encoded_packet: &[u8], - ) -> Result<(Vec, u8, Vec, Vec, usize), PacketDecodeErr> { + ) -> Result { // static header let mut static_header = encoded_packet[IV_MASKING_SIZE..STATIC_HEADER_END].to_vec(); @@ -129,7 +140,7 @@ impl Packet { let version = u16::from_be_bytes(static_header[6..8].try_into()?); if protocol_id != PROTOCOL_ID || version != PROTOCOL_VERSION { return Err(PacketDecodeErr::InvalidProtocol( - match str::from_utf8(&protocol_id) { + match str::from_utf8(protocol_id) { Ok(result) => format!("{} v{}", result, version), Err(_) => format!("{:?} v{}", protocol_id, version), }, @@ -143,7 +154,14 @@ impl Packet { let authdata = &mut encoded_packet[STATIC_HEADER_END..authdata_end].to_vec(); cipher.try_apply_keystream(authdata)?; - Ok((static_header, flag, nonce, authdata.to_vec(), authdata_end)) + + Ok(PacketHeader { + static_header, + flag, + nonce, + authdata: authdata.to_vec(), + header_end_offset: authdata_end, + }) } } @@ -158,7 +176,7 @@ impl Ordinary { static_header: Vec, authdata: Vec, nonce: Vec, - decrypt_key: &Vec, + decrypt_key: &[u8], encrypted_message: &[u8], ) -> Result { // message = aesgcm_encrypt(initiator-key, nonce, message-pt, message-ad) @@ -168,7 +186,7 @@ impl Ordinary { message_ad.extend_from_slice(&static_header); message_ad.extend_from_slice(&authdata); - let mut message = (&encrypted_message).to_vec(); + let mut message = encrypted_message.to_vec(); Self::decrypt(decrypt_key, nonce, &mut message, message_ad)?; let message = Message::decode(&message)?; @@ -176,7 +194,7 @@ impl Ordinary { } fn decrypt( - key: &Vec, + key: &[u8], nonce: Vec, message: &mut Vec, message_ad: Vec, @@ -224,7 +242,7 @@ impl WhoAreYou { buf.put_slice(&self.enr_seq.to_be_bytes()); } - pub fn decode(authdata: &Vec) -> Result { + pub fn decode(authdata: &[u8]) -> Result { let id_nonce = u128::from_be_bytes(authdata[..16].try_into()?); let enr_seq = u64::from_be_bytes(authdata[16..].try_into()?); @@ -491,7 +509,7 @@ mod tests { let packet = Packet::WhoAreYou(WhoAreYou { id_nonce: u128::from_be_bytes( - (&hex!("0102030405060708090a0b0c0d0e0f10")) + hex!("0102030405060708090a0b0c0d0e0f10") .to_vec() .try_into() .unwrap(), @@ -540,7 +558,7 @@ mod tests { let packet = codec.decode(&mut encoded).unwrap(); let expected = Some(Packet::WhoAreYou(WhoAreYou { id_nonce: u128::from_be_bytes( - (&hex!("0102030405060708090a0b0c0d0e0f10")) + hex!("0102030405060708090a0b0c0d0e0f10") .to_vec() .try_into() .unwrap(), @@ -633,15 +651,14 @@ mod tests { id: Some("id".to_string()), ..Default::default() } - .try_into() - .unwrap(); + .into(); let pkt = NodesMessage { req_id: 1234, total: 2, nodes: vec![NodeRecord { seq: 4321, - pairs: pairs, + pairs, signature: H512::random(), }], }; From e247d48cfb90bfa4907df433777138558e62c93b Mon Sep 17 00:00:00 2001 From: Edgar Date: Tue, 16 Dec 2025 14:17:32 +0100 Subject: [PATCH 16/29] chore(l1): improve discv5 new_nonce (#5652) **Motivation** I saw current new_nonce impl allocated a vector when it can just return a fixed size array. **Description** Removes the needless vec **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. --- crates/networking/p2p/discv5/codec.rs | 8 +++++--- crates/networking/p2p/discv5/messages.rs | 13 ++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/networking/p2p/discv5/codec.rs b/crates/networking/p2p/discv5/codec.rs index d39510bc02d..1f6543d823e 100644 --- a/crates/networking/p2p/discv5/codec.rs +++ b/crates/networking/p2p/discv5/codec.rs @@ -20,9 +20,11 @@ impl Discv5Codec { } } - fn new_nonce(&mut self) -> Vec { + fn new_nonce(&mut self) -> [u8; 12] { self.nonce = self.nonce.wrapping_add(1); - self.nonce.to_be_bytes()[4..].to_vec() + let mut bytes = [0u8; 12]; + bytes.copy_from_slice(&self.nonce.to_be_bytes()[4..]); + bytes } } @@ -49,6 +51,6 @@ impl Encoder for Discv5Codec { fn encode(&mut self, package: Packet, buf: &mut BytesMut) -> Result<(), Self::Error> { let masking_iv: u128 = rand::random(); let nonce = self.new_nonce(); - package.encode(buf, masking_iv, nonce, &self.dest_id) + package.encode(buf, masking_iv, &nonce, &self.dest_id) } } diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 602fb7c1799..cb243413773 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -107,7 +107,7 @@ impl Packet { &self, buf: &mut dyn BufMut, masking_iv: u128, - nonce: Vec, + nonce: &[u8], dest_id: &H256, ) -> Result<(), PacketDecodeErr> { let masking_as_bytes = masking_iv.to_be_bytes(); @@ -218,13 +218,13 @@ impl WhoAreYou { &self, buf: &mut dyn BufMut, cipher: &mut T, - nonce: Vec, + nonce: &[u8], ) -> Result<(), PacketDecodeErr> { let mut static_header = Vec::new(); static_header.put_slice(PROTOCOL_ID); static_header.put_slice(&PROTOCOL_VERSION.to_be_bytes()); static_header.put_u8(0x01); - static_header.put_slice(&nonce); + static_header.put_slice(nonce); static_header.put_slice(&24u16.to_be_bytes()); cipher.try_apply_keystream(&mut static_header)?; buf.put_slice(&static_header); @@ -520,12 +520,7 @@ mod tests { let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); let mut buf = Vec::new(); - let _ = packet.encode( - &mut buf, - 0, - hex!("0102030405060708090a0b0c").to_vec(), - &dest_id, - ); + let _ = packet.encode(&mut buf, 0, &hex!("0102030405060708090a0b0c"), &dest_id); let expected = &hex!( "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" ); From 13b1e16d4b7cbf86446f075b3561462154c979a3 Mon Sep 17 00:00:00 2001 From: Edgar Date: Tue, 16 Dec 2025 14:19:09 +0100 Subject: [PATCH 17/29] chore(l1): put all discv5 behind a feature flag (#5651) **Motivation** In order to start merging discv5 code into main, to avoid having a huge PR at the end of the development, we should create a feature flag disabled by default. Closes #5639 --- cmd/ethrex/Cargo.toml | 2 ++ crates/networking/p2p/Cargo.toml | 2 ++ crates/networking/p2p/p2p.rs | 1 + 3 files changed, 5 insertions(+) diff --git a/cmd/ethrex/Cargo.toml b/cmd/ethrex/Cargo.toml index 7ed5b498e11..f37c8fba1d9 100644 --- a/cmd/ethrex/Cargo.toml +++ b/cmd/ethrex/Cargo.toml @@ -101,6 +101,8 @@ jemalloc_profiling = [ "ethrex-rpc/jemalloc_profiling", ] sync-test = ["ethrex-p2p/sync-test"] +# discv5 is currently experimental and should only be enabled for development purposes +discv5 = ["ethrex-p2p/discv5"] l2 = [ "ethrex-l2", diff --git a/crates/networking/p2p/Cargo.toml b/crates/networking/p2p/Cargo.toml index 72b1ce5d9a6..750c3820179 100644 --- a/crates/networking/p2p/Cargo.toml +++ b/crates/networking/p2p/Cargo.toml @@ -66,6 +66,8 @@ sync-test = [] l2 = ["dep:ethrex-storage-rollup"] test-utils = [] metrics = ["dep:ethrex-metrics"] +# discv5 is currently experimental and should only be enabled for development purposes +discv5 = [] [lints.clippy] unwrap_used = "deny" diff --git a/crates/networking/p2p/p2p.rs b/crates/networking/p2p/p2p.rs index cd9e5d39154..b71b2f98230 100644 --- a/crates/networking/p2p/p2p.rs +++ b/crates/networking/p2p/p2p.rs @@ -1,4 +1,5 @@ pub mod discv4; +#[cfg(feature = "discv5")] pub mod discv5; pub(crate) mod metrics; pub mod network; From 48282dacb839fafed377dae655e81ddcf5b28bb8 Mon Sep 17 00:00:00 2001 From: Edgar Date: Tue, 16 Dec 2025 14:20:03 +0100 Subject: [PATCH 18/29] feat(l1): implement discv5 TICKET message codec (#5650) **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. Closes #5586 Closes #5587 --- crates/networking/p2p/discv5/messages.rs | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index cb243413773..f63b4e93915 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -257,6 +257,7 @@ pub enum Message { FindNode(FindNodeMessage), Nodes(NodesMessage), TalkReq(TalkReqMessage), + Ticket(TicketMessage), // TODO: add the other messages } @@ -288,6 +289,10 @@ impl Message { // let (enr_response_msg, _rest) = ENRResponseMessage::decode_unfinished(msg)?; // Ok(Message::ENRResponse(enr_response_msg)) // } + 0x08 => { + let ticket_msg = TicketMessage::decode(&encrypted_message[1..])?; + Ok(Message::Ticket(ticket_msg)) + } _ => Err(RLPDecodeError::MalformedData), } } @@ -462,6 +467,41 @@ impl RLPDecode for TalkReqMessage { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TicketMessage { + pub req_id: u64, + pub ticket: Bytes, + pub wait_time: u64, +} + +impl RLPEncode for TicketMessage { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.req_id) + .encode_field(&self.ticket) + .encode_field(&self.wait_time) + .finish(); + } +} + +impl RLPDecode for TicketMessage { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (req_id, decoder) = decoder.decode_field("req_id")?; + let (ticket, decoder) = decoder.decode_field("ticket")?; + let (wait_time, decoder) = decoder.decode_field("wait_time")?; + + Ok(( + Self { + req_id, + ticket, + wait_time, + }, + decoder.finish()?, + )) + } +} + #[cfg(test)] mod tests { use super::*; @@ -673,4 +713,16 @@ mod tests { let buf = pkt.encode_to_vec(); assert_eq!(TalkReqMessage::decode(&buf).unwrap(), pkt); } + + #[test] + fn ticket_packet_codec_roundtrip() { + let pkt = TicketMessage { + req_id: 1234, + ticket: Bytes::from_static(&[1, 2, 3, 4]), + wait_time: 5, + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(TicketMessage::decode(&buf).unwrap(), pkt); + } } From 2907a37d6b784d92291da6c6e44057912a48e035 Mon Sep 17 00:00:00 2001 From: MrAzteca Date: Tue, 16 Dec 2025 15:03:30 +0100 Subject: [PATCH 19/29] feat(l1): implement `discv5` `TalkRes` message codec (#5632) **Motivation** **Description** **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. --- crates/networking/p2p/discv5/messages.rs | 48 ++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index f63b4e93915..f1db2739092 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -257,6 +257,7 @@ pub enum Message { FindNode(FindNodeMessage), Nodes(NodesMessage), TalkReq(TalkReqMessage), + TalkRes(TalkResMessage), Ticket(TicketMessage), // TODO: add the other messages } @@ -285,10 +286,10 @@ impl Message { let talk_req_msg = TalkReqMessage::decode(&encrypted_message[1..])?; Ok(Message::TalkReq(talk_req_msg)) } - // 0x06 => { - // let (enr_response_msg, _rest) = ENRResponseMessage::decode_unfinished(msg)?; - // Ok(Message::ENRResponse(enr_response_msg)) - // } + 0x06 => { + let enr_response_msg = TalkResMessage::decode(&encrypted_message[1..])?; + Ok(Message::TalkRes(enr_response_msg)) + } 0x08 => { let ticket_msg = TicketMessage::decode(&encrypted_message[1..])?; Ok(Message::Ticket(ticket_msg)) @@ -467,6 +468,35 @@ impl RLPDecode for TalkReqMessage { } } +#[derive(Debug, Clone, PartialEq, Eq)] +struct TalkResMessage { + pub req_id: u64, + pub response: Vec, +} + +impl RLPEncode for TalkResMessage { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.req_id) + .encode_field(&Bytes::copy_from_slice(&self.response)) + .finish(); + } +} + +impl RLPDecode for TalkResMessage { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let ((req_id, response), remaining) = <(u64, Bytes) as RLPDecode>::decode_unfinished(rlp)?; + + Ok(( + Self { + req_id, + response: response.to_vec(), + }, + remaining, + )) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TicketMessage { pub req_id: u64, @@ -714,6 +744,16 @@ mod tests { assert_eq!(TalkReqMessage::decode(&buf).unwrap(), pkt); } + fn talk_res_packet_codec_roundtrip() { + let pkt = TalkResMessage { + req_id: 1234, + response: b"\x00\x01\x02\x03".into(), + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(TalkResMessage::decode(&buf).unwrap(), pkt); + } + #[test] fn ticket_packet_codec_roundtrip() { let pkt = TicketMessage { From 4aae22cdf1a0c459eec112d927e7a18a9c714d5e Mon Sep 17 00:00:00 2001 From: Edgar Date: Tue, 16 Dec 2025 22:58:59 +0100 Subject: [PATCH 20/29] feat(l1): implement discv5 handshake encoding/decoding (#5653) **Motivation** **Description** **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. Closes #5570 Closes #5571 --- crates/networking/p2p/discv5/codec.rs | 2 +- crates/networking/p2p/discv5/messages.rs | 311 ++++++++++++++++++++++- 2 files changed, 303 insertions(+), 10 deletions(-) diff --git a/crates/networking/p2p/discv5/codec.rs b/crates/networking/p2p/discv5/codec.rs index 1f6543d823e..eeab5c3a293 100644 --- a/crates/networking/p2p/discv5/codec.rs +++ b/crates/networking/p2p/discv5/codec.rs @@ -51,6 +51,6 @@ impl Encoder for Discv5Codec { fn encode(&mut self, package: Packet, buf: &mut BytesMut) -> Result<(), Self::Error> { let masking_iv: u128 = rand::random(); let nonce = self.new_nonce(); - package.encode(buf, masking_iv, &nonce, &self.dest_id) + package.encode(buf, masking_iv, &nonce, &self.dest_id, &self.key) } } diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index f1db2739092..cff101c45c2 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -10,7 +10,6 @@ use ethrex_rlp::{ error::RLPDecodeError, structs::{Decoder, Encoder}, }; -use secp256k1::SecretKey; use crate::types::NodeRecord; @@ -21,6 +20,8 @@ type Aes128Ctr64BE = ctr::Ctr64BE; // Used for package validation const MIN_PACKET_SIZE: usize = 63; const MAX_PACKET_SIZE: usize = 1280; +/// 32 src-id + 1 sig-size + 1 eph-key-size +const HANDSHAKE_AUTHDATA_HEAD: usize = 34; // protocol data const PROTOCOL_ID: &[u8] = b"discv5"; const PROTOCOL_VERSION: u16 = 0x0001; @@ -55,7 +56,7 @@ impl From for PacketDecodeErr { pub enum Packet { Ordinary(Ordinary), WhoAreYou(WhoAreYou), - // Handshake(Handshake), + Handshake(Handshake), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -86,6 +87,7 @@ impl Packet { let mut cipher = ::new(dest_id[..16].into(), masking_iv.into()); let packet_header = Packet::decode_header(&mut cipher, encoded_packet)?; + let encrypted_message = &encoded_packet[packet_header.header_end_offset..]; match packet_header.flag { 0x00 => Ok(Packet::Ordinary(Ordinary::decode( @@ -94,11 +96,17 @@ impl Packet { packet_header.authdata, packet_header.nonce, decrypt_key, - &encoded_packet[packet_header.header_end_offset..], + encrypted_message, )?)), 0x01 => Ok(Packet::WhoAreYou(WhoAreYou::decode( &packet_header.authdata, )?)), + 0x02 => Ok(Packet::Handshake(Handshake::decode( + masking_iv, + packet_header, + decrypt_key, + encrypted_message, + )?)), _ => Err(RLPDecodeError::MalformedData)?, } } @@ -109,6 +117,7 @@ impl Packet { masking_iv: u128, nonce: &[u8], dest_id: &H256, + encrypt_key: &[u8], ) -> Result<(), PacketDecodeErr> { let masking_as_bytes = masking_iv.to_be_bytes(); buf.put_slice(&masking_as_bytes); @@ -121,6 +130,16 @@ impl Packet { Packet::WhoAreYou(who_are_you) => { who_are_you.encode_header(buf, &mut cipher, nonce)?; } + Packet::Handshake(handshake) => { + let (mut static_header, mut authdata, encrypted_message) = + handshake.encode(&nonce, &masking_as_bytes, encrypt_key)?; + + cipher.try_apply_keystream(&mut static_header)?; + buf.put_slice(&static_header); + cipher.try_apply_keystream(&mut authdata)?; + buf.put_slice(&authdata); + buf.put_slice(&encrypted_message); + } } Ok(()) } @@ -250,6 +269,143 @@ impl WhoAreYou { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Handshake { + pub src_id: H256, + pub id_signature: Vec, + pub eph_pubkey: Vec, + /// The record field may be omitted if the enr-seq of WHOAREYOU is recent enough, i.e. when it matches the current sequence number of the sending node. + /// If enr-seq is zero, the record must be sent. + pub record: Option, + pub message: Message, +} + +impl Handshake { + fn encode_authdata(&self, buf: &mut dyn BufMut) -> Result<(), PacketDecodeErr> { + let sig_size: u8 = self + .id_signature + .len() + .try_into() + .map_err(|_| PacketDecodeErr::InvalidSize)?; + let eph_key_size: u8 = self + .eph_pubkey + .len() + .try_into() + .map_err(|_| PacketDecodeErr::InvalidSize)?; + + buf.put_slice(self.src_id.as_bytes()); + buf.put_u8(sig_size); + buf.put_u8(eph_key_size); + buf.put_slice(&self.id_signature); + buf.put_slice(&self.eph_pubkey); + if let Some(record) = &self.record { + record.encode(buf); + } + + Ok(()) + } + + /// Encodes the handshake returning the header, authdata and encrypted_message + #[allow(clippy::type_complexity)] + fn encode( + &self, + nonce: &[u8], + masking_iv: &[u8], + encrypt_key: &[u8], + ) -> Result<(Vec, Vec, Vec), PacketDecodeErr> { + let mut authdata = Vec::new(); + self.encode_authdata(&mut authdata)?; + + let authdata_size = + u16::try_from(authdata.len()).map_err(|_| PacketDecodeErr::InvalidSize)?; + + let mut static_header = Vec::new(); + static_header.put_slice(PROTOCOL_ID); + static_header.put_slice(&PROTOCOL_VERSION.to_be_bytes()); + static_header.put_u8(0x02); + static_header.put_slice(nonce); + static_header.put_slice(&authdata_size.to_be_bytes()); + + let mut message = Vec::new(); + self.message.encode(&mut message); + + if encrypt_key.len() < 16 { + return Err(PacketDecodeErr::InvalidSize); + } + + let mut message_ad = masking_iv.to_vec(); + message_ad.extend_from_slice(&static_header); + message_ad.extend_from_slice(&authdata); + + let mut cipher = Aes128Gcm::new(encrypt_key[..16].into()); + cipher + .encrypt_in_place(nonce.into(), &message_ad, &mut message) + .map_err(|e| PacketDecodeErr::ChipherError(e.to_string()))?; + + Ok((static_header, authdata, message)) + } + + #[allow(clippy::too_many_arguments)] + pub fn decode( + masking_iv: &[u8], + header: PacketHeader, + decrypt_key: &[u8], + encrypted_message: &[u8], + ) -> Result { + let PacketHeader { + static_header, + nonce, + authdata, + .. + } = header; + + if authdata.len() < HANDSHAKE_AUTHDATA_HEAD { + return Err(PacketDecodeErr::InvalidSize); + } + + let src_id = H256::from_slice(&authdata[..32]); + let sig_size = authdata[32] as usize; + let eph_key_size = authdata[33] as usize; + + let authdata_head = HANDSHAKE_AUTHDATA_HEAD + sig_size + eph_key_size; + if authdata.len() < authdata_head { + return Err(PacketDecodeErr::InvalidSize); + } + + let id_signature = + authdata[HANDSHAKE_AUTHDATA_HEAD..HANDSHAKE_AUTHDATA_HEAD + sig_size].to_vec(); + let eph_key_start = HANDSHAKE_AUTHDATA_HEAD + sig_size; + let eph_pubkey = authdata[eph_key_start..authdata_head].to_vec(); + + let record = if authdata.len() > authdata_head { + let record_bytes = &authdata[authdata_head..]; + if record_bytes.is_empty() { + None + } else { + Some(NodeRecord::decode(record_bytes)?) + } + } else { + None + }; + + let mut message_ad = masking_iv.to_vec(); + message_ad.extend_from_slice(&static_header); + message_ad.extend_from_slice(&authdata); + + let mut message = encrypted_message.to_vec(); + Ordinary::decrypt(decrypt_key, nonce, &mut message, message_ad)?; + let message = Message::decode(&message)?; + + Ok(Handshake { + src_id, + id_signature, + eph_pubkey, + record, + message, + }) + } +} + #[derive(Debug, Eq, PartialEq, Clone)] pub enum Message { Ping(PingMessage), @@ -263,6 +419,31 @@ pub enum Message { } impl Message { + fn msg_type(&self) -> u8 { + match self { + Message::Ping(_) => 0x01, + Message::Pong(_) => 0x02, + Message::FindNode(_) => 0x03, + Message::Nodes(_) => 0x04, + Message::TalkReq(_) => 0x05, + Message::TalkRes(_) => 0x06, + Message::Ticket(_) => 0x08, + } + } + + pub fn encode(&self, buf: &mut dyn BufMut) { + buf.put_u8(self.msg_type()); + match self { + Message::Ping(ping) => ping.encode(buf), + Message::Pong(pong) => pong.encode(buf), + Message::FindNode(find_node) => find_node.encode(buf), + Message::Nodes(nodes) => nodes.encode(buf), + Message::TalkReq(talk_req) => talk_req.encode(buf), + Message::TalkRes(talk_res) => talk_res.encode(buf), + Message::Ticket(ticket) => ticket.encode(buf), + } + } + pub fn decode(encrypted_message: &[u8]) -> Result { let message_type = encrypted_message[0]; match message_type { @@ -297,10 +478,6 @@ impl Message { _ => Err(RLPDecodeError::MalformedData), } } - - pub fn encode(&self, _buf: &mut dyn BufMut, _signer: &SecretKey) { - //TODO - } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -469,7 +646,7 @@ impl RLPDecode for TalkReqMessage { } #[derive(Debug, Clone, PartialEq, Eq)] -struct TalkResMessage { +pub struct TalkResMessage { pub req_id: u64, pub response: Vec, } @@ -561,6 +738,115 @@ mod tests { // )) // .unwrap(); + #[test] + fn handshake_packet_roundtrip() { + let node_a_key = SecretKey::from_byte_array(&hex!( + "eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f" + )) + .unwrap(); + let node_b_key = SecretKey::from_byte_array(&hex!( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" + )) + .unwrap(); + + let src_id = node_id(&public_key_from_signing_key(&node_a_key)); + let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); + + let handshake = Handshake { + src_id, + id_signature: vec![1; 64], + eph_pubkey: vec![2; 33], + record: None, + message: Message::Ping(PingMessage { + req_id: vec![3], + enr_seq: 4, + }), + }; + + let key = vec![0x10; 16]; + let nonce = hex!("000102030405060708090a0b").to_vec(); + let mut buf = Vec::new(); + let packet = Packet::Handshake(handshake.clone()); + packet.encode(&mut buf, 0, &nonce, &dest_id, &key).unwrap(); + + let decoded = Packet::decode(&dest_id, &key, &buf).unwrap(); + assert_eq!(decoded, Packet::Handshake(handshake)); + } + + /// Ping handshake packet (flag 2) from https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md + #[test] + fn handshake_packet_vector_test_roundtrip() { + /* + # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb + # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 + # nonce = 0xffffffffffffffffffffffff + # read-key = 0x4f9fac6de7567d1e3b1241dffe90f662 + # ping.req-id = 0x00000001 + # ping.enr-seq = 1 + # + # handshake inputs: + # + # whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000001 + # whoareyou.request-nonce = 0x0102030405060708090a0b0c + # whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10 + # whoareyou.enr-seq = 1 + # ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6 + # ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5 + + 00000000000000000000000000000000088b3d4342774649305f313964a39e55 + ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 + 4c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef + 268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfb + a776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1 + f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d83 + 9cf8 + */ + let node_b_key = SecretKey::from_byte_array(&hex!( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" + )) + .unwrap(); + let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); + + let encoded = &hex!( + "00000000000000000000000000000000088b3d4342774649305f313964a39e55ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfba776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d839cf8" + ); + let read_key = hex!("4f9fac6de7567d1e3b1241dffe90f662").to_vec(); + + let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); + let handshake = match packet { + Packet::Handshake(hs) => hs, + other => panic!("unexpected packet {other:?}"), + }; + + assert_eq!( + handshake.src_id, + H256::from_slice(&hex!( + "aaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb" + )) + ); + assert_eq!(handshake.record, None); + assert_eq!( + handshake.eph_pubkey, + hex!("039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5").to_vec() + ); + assert_eq!( + handshake.message, + Message::Ping(PingMessage { + req_id: hex!("00000001").to_vec(), + enr_seq: 1, + }) + ); + + let masking_iv = u128::from_be_bytes(encoded[..16].try_into().unwrap()); + let nonce = hex!("ffffffffffffffffffffffff").to_vec(); + let mut buf = Vec::new(); + Packet::Handshake(handshake) + .encode(&mut buf, masking_iv, &nonce, &dest_id, &read_key) + .unwrap(); + + assert_eq!(buf, encoded.to_vec()); + } + #[test] fn test_encode_whoareyou_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb @@ -590,7 +876,13 @@ mod tests { let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); let mut buf = Vec::new(); - let _ = packet.encode(&mut buf, 0, &hex!("0102030405060708090a0b0c"), &dest_id); + let _ = packet.encode( + &mut buf, + 0, + &hex!("0102030405060708090a0b0c"), + &dest_id, + &[], + ); let expected = &hex!( "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" ); @@ -744,6 +1036,7 @@ mod tests { assert_eq!(TalkReqMessage::decode(&buf).unwrap(), pkt); } + #[test] fn talk_res_packet_codec_roundtrip() { let pkt = TalkResMessage { req_id: 1234, From f2e850176e2b9b11ec3cd9ba208fe0a493c96d59 Mon Sep 17 00:00:00 2001 From: Edgar Date: Wed, 17 Dec 2025 16:00:30 +0100 Subject: [PATCH 21/29] feat(l1): discv5, add ordinary packet coding (#5665) **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. Closes #5566 Closes #5567 --- crates/networking/p2p/discv5/messages.rs | 167 +++++++++++++++++++++-- 1 file changed, 159 insertions(+), 8 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index cff101c45c2..e55c5911837 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -126,7 +126,16 @@ impl Packet { ::new(dest_id[..16].into(), masking_as_bytes[..].into()); match self { - Packet::Ordinary(_ordinary) => todo!(), + Packet::Ordinary(ordinary) => { + let (mut static_header, mut authdata, encrypted_message) = + ordinary.encode(&nonce, &masking_as_bytes, encrypt_key)?; + + cipher.try_apply_keystream(&mut static_header)?; + buf.put_slice(&static_header); + cipher.try_apply_keystream(&mut authdata)?; + buf.put_slice(&authdata); + buf.put_slice(&encrypted_message); + } Packet::WhoAreYou(who_are_you) => { who_are_you.encode_header(buf, &mut cipher, nonce)?; } @@ -186,10 +195,56 @@ impl Packet { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Ordinary { - message: Message, + pub src_id: H256, + pub message: Message, } impl Ordinary { + fn encode_authdata(&self, buf: &mut dyn BufMut) -> Result<(), PacketDecodeErr> { + buf.put_slice(self.src_id.as_bytes()); + Ok(()) + } + + /// Encodes the ordinary packet returning the header, authdata and encrypted_message + #[allow(clippy::type_complexity)] + fn encode( + &self, + nonce: &[u8], + masking_iv: &[u8], + encrypt_key: &[u8], + ) -> Result<(Vec, Vec, Vec), PacketDecodeErr> { + if encrypt_key.len() < 16 { + return Err(PacketDecodeErr::InvalidSize); + } + + let mut authdata = Vec::new(); + self.encode_authdata(&mut authdata)?; + + let authdata_size: u16 = + u16::try_from(authdata.len()).map_err(|_| PacketDecodeErr::InvalidSize)?; + + let mut static_header = Vec::new(); + static_header.put_slice(PROTOCOL_ID); + static_header.put_slice(&PROTOCOL_VERSION.to_be_bytes()); + static_header.put_u8(0x0); + static_header.put_slice(nonce); + static_header.put_slice(&authdata_size.to_be_bytes()); + + let mut message = Vec::new(); + self.message.encode(&mut message); + + let mut message_ad = masking_iv.to_vec(); + message_ad.extend_from_slice(&static_header); + message_ad.extend_from_slice(&authdata); + + let mut cipher = Aes128Gcm::new(encrypt_key[..16].into()); + cipher + .encrypt_in_place(nonce.into(), &message_ad, &mut message) + .map_err(|e| PacketDecodeErr::ChipherError(e.to_string()))?; + + Ok((static_header, authdata, message)) + } + pub fn decode( masking_iv: &[u8], static_header: Vec, @@ -198,6 +253,10 @@ impl Ordinary { decrypt_key: &[u8], encrypted_message: &[u8], ) -> Result { + if authdata.len() != 32 { + return Err(PacketDecodeErr::InvalidSize); + } + // message = aesgcm_encrypt(initiator-key, nonce, message-pt, message-ad) // message-pt = message-type || message-data // message-ad = masking-iv || header @@ -208,8 +267,10 @@ impl Ordinary { let mut message = encrypted_message.to_vec(); Self::decrypt(decrypt_key, nonce, &mut message, message_ad)?; + let src_id = H256::from_slice(&authdata); + let message = Message::decode(&message)?; - Ok(Ordinary { message }) + Ok(Ordinary { src_id, message }) } fn decrypt( @@ -847,6 +908,59 @@ mod tests { assert_eq!(buf, encoded.to_vec()); } + /// Ping handshake message packet (flag 2, with ENR) from https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md + #[test] + fn handshake_packet_with_enr_vector_test_roundtrip() { + let node_b_key = SecretKey::from_byte_array(&hex!( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" + )) + .unwrap(); + let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); + + let encoded = &hex!( + "00000000000000000000000000000000088b3d4342774649305f313964a39e55ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be98562fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b21481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb12a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b139471" + ); + let nonce = hex!("ffffffffffffffffffffffff").to_vec(); + let read_key = hex!("53b1c075f41876423154e157470c2f48").to_vec(); + + let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); + let handshake = match packet { + Packet::Handshake(hs) => hs, + other => panic!("unexpected packet {other:?}"), + }; + + assert_eq!( + handshake.src_id, + H256::from_slice(&hex!( + "aaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb" + )) + ); + assert_eq!( + handshake.eph_pubkey, + hex!("039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5").to_vec() + ); + assert_eq!( + handshake.message, + Message::Ping(PingMessage { + req_id: hex!("00000001").to_vec(), + enr_seq: 1, + }) + ); + + let record = handshake.record.clone().expect("expected ENR record"); + let pairs = record.decode_pairs(); + assert_eq!(pairs.id.as_deref(), Some("v4")); + assert!(pairs.secp256k1.is_some()); + + let masking_iv = u128::from_be_bytes(encoded[..16].try_into().unwrap()); + let mut buf = Vec::new(); + Packet::Handshake(handshake) + .encode(&mut buf, masking_iv, &nonce, &dest_id, &read_key) + .unwrap(); + + assert_eq!(buf, encoded.to_vec()); + } + #[test] fn test_encode_whoareyou_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb @@ -926,11 +1040,6 @@ mod tests { assert_eq!(packet, expected); } - #[test] - fn test_encode_ping_message() { - // TODO - } - #[test] fn test_decode_ping_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb @@ -943,11 +1052,17 @@ mod tests { // 00000000000000000000000000000000088b3d4342774649325f313964a39e55 // ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3 // 4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc + + let node_a_key = SecretKey::from_byte_array(&hex!( + "eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f" + )) + .unwrap(); let node_b_key = SecretKey::from_byte_array(&hex!( "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" )) .unwrap(); + let src_id = node_id(&public_key_from_signing_key(&node_a_key)); let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); let encoded = &hex!( @@ -957,6 +1072,7 @@ mod tests { let read_key = [0; 16].to_vec(); let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); let expected = Packet::Ordinary(Ordinary { + src_id, message: Message::Ping(PingMessage { req_id: hex!("00000001").to_vec(), enr_seq: 2, @@ -966,6 +1082,41 @@ mod tests { assert_eq!(packet, expected); } + /// Ping message packet (flag 0) from https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md + #[test] + fn ordinary_ping_packet_vector_test_roundtrip() { + let node_b_key = SecretKey::from_byte_array(&hex!( + "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" + )) + .unwrap(); + let dest_id = node_id(&public_key_from_signing_key(&node_b_key)); + + let encoded = &hex!( + "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" + ); + let nonce = hex!("ffffffffffffffffffffffff").to_vec(); + let read_key = [0; 16].to_vec(); + + let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); + let expected = Packet::Ordinary(Ordinary { + src_id: H256::from_slice(&hex!( + "aaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb" + )), + message: Message::Ping(PingMessage { + req_id: hex!("00000001").to_vec(), + enr_seq: 2, + }), + }); + assert_eq!(packet, expected); + + let masking_iv = u128::from_be_bytes(encoded[..16].try_into().unwrap()); + let mut buf = Vec::new(); + packet + .encode(&mut buf, masking_iv, &nonce, &dest_id, &read_key) + .unwrap(); + assert_eq!(buf, encoded.to_vec()); + } + #[test] fn ping_packet_codec_roundtrip() { let pkt = PingMessage { From bb8e46a61f76698d8a8d6352fdf0adc13c65cbd4 Mon Sep 17 00:00:00 2001 From: Edgar Date: Thu, 18 Dec 2025 15:07:27 +0100 Subject: [PATCH 22/29] feat(l1): add discv5 session structures and remaining official vector tests (#5673) **Motivation** Adds a Session struct and related functions to the discv5 module, this represents a session established from a handshake. This PR does not implement the handshake protocol, but it lays a path to doing so easily. This is based on reading https://github.com/ethereum/devp2p/blob/master/discv5/discv5-theory.md#sessions This allows us to add the remaining test vectors related to cryptography, which is done now. Also renamed the PacketDecodeErr to PacketCodecError, since the error was also used when encoding and for other things. Fixed nonce generation to be secure according to the spec --- Cargo.lock | 10 ++ crates/l2/tee/quote-gen/Cargo.lock | 10 ++ crates/networking/p2p/Cargo.toml | 2 + crates/networking/p2p/discv5/codec.rs | 66 ++++++-- crates/networking/p2p/discv5/messages.rs | 86 ++++++---- crates/networking/p2p/discv5/mod.rs | 1 + crates/networking/p2p/discv5/session.rs | 196 +++++++++++++++++++++++ 7 files changed, 326 insertions(+), 45 deletions(-) create mode 100644 crates/networking/p2p/discv5/session.rs diff --git a/Cargo.lock b/Cargo.lock index d80bc41be1b..010c13f79b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3925,6 +3925,7 @@ dependencies = [ "futures", "hex", "hex-literal 0.4.1", + "hkdf", "hmac", "indexmap 2.12.1", "lazy_static", @@ -5211,6 +5212,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" diff --git a/crates/l2/tee/quote-gen/Cargo.lock b/crates/l2/tee/quote-gen/Cargo.lock index 38889b36d21..10c7b5a625f 100644 --- a/crates/l2/tee/quote-gen/Cargo.lock +++ b/crates/l2/tee/quote-gen/Cargo.lock @@ -2363,6 +2363,7 @@ dependencies = [ "ethrex-trie", "futures", "hex", + "hkdf", "hmac", "indexmap 2.11.4", "lazy_static", @@ -3096,6 +3097,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" diff --git a/crates/networking/p2p/Cargo.toml b/crates/networking/p2p/Cargo.toml index 750c3820179..e53f51baae9 100644 --- a/crates/networking/p2p/Cargo.toml +++ b/crates/networking/p2p/Cargo.toml @@ -50,6 +50,8 @@ aes-gcm = "0.10.3" ctr = "0.9.2" rand = "0.8.5" +hkdf = "0.12.4" + rayon = "1.10.0" crossbeam.workspace = true diff --git a/crates/networking/p2p/discv5/codec.rs b/crates/networking/p2p/discv5/codec.rs index eeab5c3a293..33bd83ca3fb 100644 --- a/crates/networking/p2p/discv5/codec.rs +++ b/crates/networking/p2p/discv5/codec.rs @@ -1,42 +1,68 @@ -use crate::discv5::messages::{Packet, PacketDecodeErr}; +use crate::discv5::messages::{Packet, PacketCodecError}; +use crate::discv5::session::Session; use bytes::BytesMut; use ethrex_common::H256; +use rand::{RngCore, thread_rng}; use tokio_util::codec::{Decoder, Encoder}; #[derive(Debug)] pub struct Discv5Codec { dest_id: H256, - nonce: u128, - key: Vec, + /// Outgoing message count, used for nonce generation as per the spec. + counter: u32, + session: Option, } impl Discv5Codec { pub fn new(dest_id: H256) -> Self { Self { dest_id, - nonce: rand::random(), - key: vec![], + counter: 0, + session: None, } } - fn new_nonce(&mut self) -> [u8; 12] { - self.nonce = self.nonce.wrapping_add(1); - let mut bytes = [0u8; 12]; - bytes.copy_from_slice(&self.nonce.to_be_bytes()[4..]); - bytes + pub fn with_session(dest_id: H256, session: Session) -> Self { + Self { + dest_id, + counter: 0, + session: Some(session), + } + } + + pub fn set_session(&mut self, session: Session) { + self.session = Some(session); + } + + /// Generates a 96-bit AES-GCM nonce + /// ## Spec Recommendation + /// Encode the current outgoing message count into the first 32 bits of the nonce and fill the remaining 64 bits with random data generated + /// by a cryptographically secure random number generator. + pub fn next_nonce(&mut self, rng: &mut R) -> [u8; 12] { + let counter = self.counter; + self.counter = self.counter.wrapping_add(1); + + let mut nonce = [0u8; 12]; + nonce[..4].copy_from_slice(&counter.to_be_bytes()); + rng.fill_bytes(&mut nonce[4..]); + nonce } } impl Decoder for Discv5Codec { type Item = Packet; - type Error = PacketDecodeErr; + type Error = PacketCodecError; fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { if !buf.is_empty() { + let key: &[u8] = match &self.session { + Some(session) => session.inbound_key(), + None => &[], + }; Ok(Some(Packet::decode( &self.dest_id, - &self.key, + key, &buf.split_to(buf.len()), )?)) } else { @@ -46,11 +72,19 @@ impl Decoder for Discv5Codec { } impl Encoder for Discv5Codec { - type Error = PacketDecodeErr; + type Error = PacketCodecError; - fn encode(&mut self, package: Packet, buf: &mut BytesMut) -> Result<(), Self::Error> { + fn encode(&mut self, packet: Packet, buf: &mut BytesMut) -> Result<(), Self::Error> { let masking_iv: u128 = rand::random(); - let nonce = self.new_nonce(); - package.encode(buf, masking_iv, &nonce, &self.dest_id, &self.key) + let mut rng = thread_rng(); + let nonce = self.next_nonce(&mut rng); + // key isnt needed in WHOAREYOU packets + let key = match (&packet, &mut self.session) { + (Packet::WhoAreYou(_), _) => &[][..], + (_, Some(session)) => session.outbound_key(), + (_, None) => return Err(PacketCodecError::SessionNotEstablished), + }; + + packet.encode(buf, masking_iv, &nonce, &self.dest_id, key) } } diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index e55c5911837..a46685d1bf1 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -31,11 +31,13 @@ const IV_MASKING_SIZE: usize = 16; const STATIC_HEADER_END: usize = IV_MASKING_SIZE + 23; #[derive(Debug, thiserror::Error)] -pub enum PacketDecodeErr { +pub enum PacketCodecError { #[error("RLP decoding error")] RLPDecodeError(#[from] RLPDecodeError), #[error("Invalid packet size")] InvalidSize, + #[error("Session not established yet")] + SessionNotEstablished, #[error("Invalid protocol: {0}")] InvalidProtocol(String), #[error("Stream Cipher Error: {0}")] @@ -46,9 +48,9 @@ pub enum PacketDecodeErr { IoError(#[from] std::io::Error), } -impl From for PacketDecodeErr { +impl From for PacketCodecError { fn from(error: StreamCipherError) -> Self { - PacketDecodeErr::ChipherError(error.to_string()) + PacketCodecError::ChipherError(error.to_string()) } } @@ -74,9 +76,9 @@ impl Packet { dest_id: &H256, decrypt_key: &[u8], encoded_packet: &[u8], - ) -> Result { + ) -> Result { if encoded_packet.len() < MIN_PACKET_SIZE || encoded_packet.len() > MAX_PACKET_SIZE { - return Err(PacketDecodeErr::InvalidSize); + return Err(PacketCodecError::InvalidSize); } // the packet structure is @@ -118,7 +120,7 @@ impl Packet { nonce: &[u8], dest_id: &H256, encrypt_key: &[u8], - ) -> Result<(), PacketDecodeErr> { + ) -> Result<(), PacketCodecError> { let masking_as_bytes = masking_iv.to_be_bytes(); buf.put_slice(&masking_as_bytes); @@ -156,7 +158,7 @@ impl Packet { fn decode_header( cipher: &mut T, encoded_packet: &[u8], - ) -> Result { + ) -> Result { // static header let mut static_header = encoded_packet[IV_MASKING_SIZE..STATIC_HEADER_END].to_vec(); @@ -167,7 +169,7 @@ impl Packet { let protocol_id = &static_header[..6]; let version = u16::from_be_bytes(static_header[6..8].try_into()?); if protocol_id != PROTOCOL_ID || version != PROTOCOL_VERSION { - return Err(PacketDecodeErr::InvalidProtocol( + return Err(PacketCodecError::InvalidProtocol( match str::from_utf8(protocol_id) { Ok(result) => format!("{} v{}", result, version), Err(_) => format!("{:?} v{}", protocol_id, version), @@ -200,7 +202,7 @@ pub struct Ordinary { } impl Ordinary { - fn encode_authdata(&self, buf: &mut dyn BufMut) -> Result<(), PacketDecodeErr> { + fn encode_authdata(&self, buf: &mut dyn BufMut) -> Result<(), PacketCodecError> { buf.put_slice(self.src_id.as_bytes()); Ok(()) } @@ -212,16 +214,16 @@ impl Ordinary { nonce: &[u8], masking_iv: &[u8], encrypt_key: &[u8], - ) -> Result<(Vec, Vec, Vec), PacketDecodeErr> { + ) -> Result<(Vec, Vec, Vec), PacketCodecError> { if encrypt_key.len() < 16 { - return Err(PacketDecodeErr::InvalidSize); + return Err(PacketCodecError::InvalidSize); } let mut authdata = Vec::new(); self.encode_authdata(&mut authdata)?; let authdata_size: u16 = - u16::try_from(authdata.len()).map_err(|_| PacketDecodeErr::InvalidSize)?; + u16::try_from(authdata.len()).map_err(|_| PacketCodecError::InvalidSize)?; let mut static_header = Vec::new(); static_header.put_slice(PROTOCOL_ID); @@ -240,7 +242,7 @@ impl Ordinary { let mut cipher = Aes128Gcm::new(encrypt_key[..16].into()); cipher .encrypt_in_place(nonce.into(), &message_ad, &mut message) - .map_err(|e| PacketDecodeErr::ChipherError(e.to_string()))?; + .map_err(|e| PacketCodecError::ChipherError(e.to_string()))?; Ok((static_header, authdata, message)) } @@ -252,9 +254,12 @@ impl Ordinary { nonce: Vec, decrypt_key: &[u8], encrypted_message: &[u8], - ) -> Result { + ) -> Result { if authdata.len() != 32 { - return Err(PacketDecodeErr::InvalidSize); + return Err(PacketCodecError::InvalidSize); + } + if decrypt_key.len() < 16 { + return Err(PacketCodecError::InvalidSize); } // message = aesgcm_encrypt(initiator-key, nonce, message-pt, message-ad) @@ -278,11 +283,11 @@ impl Ordinary { nonce: Vec, message: &mut Vec, message_ad: Vec, - ) -> Result<(), PacketDecodeErr> { + ) -> Result<(), PacketCodecError> { let mut cipher = Aes128Gcm::new(key[..16].into()); cipher .decrypt_in_place(nonce.as_slice().into(), &message_ad, message) - .map_err(|e| PacketDecodeErr::ChipherError(e.to_string()))?; + .map_err(|e| PacketCodecError::ChipherError(e.to_string()))?; Ok(()) } } @@ -299,7 +304,7 @@ impl WhoAreYou { buf: &mut dyn BufMut, cipher: &mut T, nonce: &[u8], - ) -> Result<(), PacketDecodeErr> { + ) -> Result<(), PacketCodecError> { let mut static_header = Vec::new(); static_header.put_slice(PROTOCOL_ID); static_header.put_slice(&PROTOCOL_VERSION.to_be_bytes()); @@ -322,7 +327,7 @@ impl WhoAreYou { buf.put_slice(&self.enr_seq.to_be_bytes()); } - pub fn decode(authdata: &[u8]) -> Result { + pub fn decode(authdata: &[u8]) -> Result { let id_nonce = u128::from_be_bytes(authdata[..16].try_into()?); let enr_seq = u64::from_be_bytes(authdata[16..].try_into()?); @@ -342,17 +347,17 @@ pub struct Handshake { } impl Handshake { - fn encode_authdata(&self, buf: &mut dyn BufMut) -> Result<(), PacketDecodeErr> { + fn encode_authdata(&self, buf: &mut dyn BufMut) -> Result<(), PacketCodecError> { let sig_size: u8 = self .id_signature .len() .try_into() - .map_err(|_| PacketDecodeErr::InvalidSize)?; + .map_err(|_| PacketCodecError::InvalidSize)?; let eph_key_size: u8 = self .eph_pubkey .len() .try_into() - .map_err(|_| PacketDecodeErr::InvalidSize)?; + .map_err(|_| PacketCodecError::InvalidSize)?; buf.put_slice(self.src_id.as_bytes()); buf.put_u8(sig_size); @@ -373,12 +378,12 @@ impl Handshake { nonce: &[u8], masking_iv: &[u8], encrypt_key: &[u8], - ) -> Result<(Vec, Vec, Vec), PacketDecodeErr> { + ) -> Result<(Vec, Vec, Vec), PacketCodecError> { let mut authdata = Vec::new(); self.encode_authdata(&mut authdata)?; let authdata_size = - u16::try_from(authdata.len()).map_err(|_| PacketDecodeErr::InvalidSize)?; + u16::try_from(authdata.len()).map_err(|_| PacketCodecError::InvalidSize)?; let mut static_header = Vec::new(); static_header.put_slice(PROTOCOL_ID); @@ -391,7 +396,7 @@ impl Handshake { self.message.encode(&mut message); if encrypt_key.len() < 16 { - return Err(PacketDecodeErr::InvalidSize); + return Err(PacketCodecError::InvalidSize); } let mut message_ad = masking_iv.to_vec(); @@ -401,7 +406,7 @@ impl Handshake { let mut cipher = Aes128Gcm::new(encrypt_key[..16].into()); cipher .encrypt_in_place(nonce.into(), &message_ad, &mut message) - .map_err(|e| PacketDecodeErr::ChipherError(e.to_string()))?; + .map_err(|e| PacketCodecError::ChipherError(e.to_string()))?; Ok((static_header, authdata, message)) } @@ -412,7 +417,10 @@ impl Handshake { header: PacketHeader, decrypt_key: &[u8], encrypted_message: &[u8], - ) -> Result { + ) -> Result { + if decrypt_key.len() < 16 { + return Err(PacketCodecError::InvalidSize); + } let PacketHeader { static_header, nonce, @@ -421,7 +429,7 @@ impl Handshake { } = header; if authdata.len() < HANDSHAKE_AUTHDATA_HEAD { - return Err(PacketDecodeErr::InvalidSize); + return Err(PacketCodecError::InvalidSize); } let src_id = H256::from_slice(&authdata[..32]); @@ -430,7 +438,7 @@ impl Handshake { let authdata_head = HANDSHAKE_AUTHDATA_HEAD + sig_size + eph_key_size; if authdata.len() < authdata_head { - return Err(PacketDecodeErr::InvalidSize); + return Err(PacketCodecError::InvalidSize); } let id_signature = @@ -781,6 +789,7 @@ mod tests { types::NodeRecordPairs, utils::{node_id, public_key_from_signing_key}, }; + use aes_gcm::{Aes128Gcm, KeyInit, aead::AeadMutInPlace}; use bytes::BytesMut; use ethrex_common::H512; use hex_literal::hex; @@ -799,6 +808,25 @@ mod tests { // )) // .unwrap(); + #[test] + fn test_aes_gcm_vector() { + // https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md#encryptiondecryption + let key = hex!("9f2d77db7004bf8a1a85107ac686990b"); + let nonce = hex!("27b5af763c446acd2749fe8e"); + let ad = hex!("93a7400fa0d6a694ebc24d5cf570f65d04215b6ac00757875e3f3a5f42107903"); + let mut pt = hex!("01c20101").to_vec(); + + let mut cipher = Aes128Gcm::new_from_slice(&key).unwrap(); + cipher + .encrypt_in_place(nonce.as_slice().into(), &ad, &mut pt) + .unwrap(); + + assert_eq!( + pt, + hex!("a5d12a2d94b8ccb3ba55558229867dc13bfa3648").to_vec() + ); + } + #[test] fn handshake_packet_roundtrip() { let node_a_key = SecretKey::from_byte_array(&hex!( diff --git a/crates/networking/p2p/discv5/mod.rs b/crates/networking/p2p/discv5/mod.rs index e623d729da5..719d9e319f2 100644 --- a/crates/networking/p2p/discv5/mod.rs +++ b/crates/networking/p2p/discv5/mod.rs @@ -1,2 +1,3 @@ pub mod codec; pub mod messages; +pub mod session; diff --git a/crates/networking/p2p/discv5/session.rs b/crates/networking/p2p/discv5/session.rs new file mode 100644 index 00000000000..5a8f2c7f3a8 --- /dev/null +++ b/crates/networking/p2p/discv5/session.rs @@ -0,0 +1,196 @@ +use ethrex_common::H256; +use hkdf::Hkdf; +use secp256k1::{ + Message as SecpMessage, PublicKey, SECP256K1, SecretKey, ecdh::shared_secret_point, + ecdsa::Signature, +}; +use sha2::{Digest, Sha256}; + +/// Role of the local node in the given session +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SessionRole { + Initiator, + Recipient, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SessionKeys { + pub initiator_key: [u8; 16], + pub recipient_key: [u8; 16], +} + +/// A discv5 session +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Session { + pub keys: SessionKeys, + pub role: SessionRole, +} + +impl Session { + pub fn new(keys: SessionKeys, role: SessionRole) -> Self { + Self { keys, role } + } + + pub fn outbound_key(&self) -> &[u8; 16] { + match self.role { + SessionRole::Initiator => &self.keys.initiator_key, + SessionRole::Recipient => &self.keys.recipient_key, + } + } + + pub fn inbound_key(&self) -> &[u8; 16] { + match self.role { + SessionRole::Initiator => &self.keys.recipient_key, + SessionRole::Recipient => &self.keys.initiator_key, + } + } +} + +/// Builds the challenge-data from a WHOAREYOU packet +pub fn build_challenge_data(masking_iv: &[u8], static_header: &[u8], authdata: &[u8]) -> Vec { + let mut data = Vec::with_capacity(masking_iv.len() + static_header.len() + authdata.len()); + data.extend_from_slice(masking_iv); + data.extend_from_slice(static_header); + data.extend_from_slice(authdata); + data +} + +/// Derives initiator/recipient keys from the handshake +pub fn derive_session_keys( + ephemeral_key: &SecretKey, + dest_pubkey: &PublicKey, + node_id_a: &H256, + node_id_b: &H256, + challenge_data: &[u8], +) -> SessionKeys { + let shared_secret = compressed_shared_secret(dest_pubkey, ephemeral_key); + let hkdf = Hkdf::::new(Some(challenge_data), &shared_secret); + + let mut kdf_info = b"discovery v5 key agreement".to_vec(); + kdf_info.extend_from_slice(node_id_a.as_bytes()); + kdf_info.extend_from_slice(node_id_b.as_bytes()); + + let mut key_data = [0u8; 32]; + hkdf.expand(&kdf_info, &mut key_data) + .expect("key_data is 32 bytes long, it can never fail"); + + SessionKeys { + initiator_key: key_data[..16].try_into().expect("sizes always match"), + recipient_key: key_data[16..].try_into().expect("sizes always match"), + } +} + +/// Signs the id-signature input used in the handshake +pub fn create_id_signature( + static_key: &SecretKey, + challenge_data: &[u8], + ephemeral_pubkey: &[u8], + node_id_b: &H256, +) -> Signature { + /* + * id-signature-text = "discovery v5 identity proof" + id-signature-input = id-signature-text || challenge-data || ephemeral-pubkey || node-id-B + id-signature = id_sign(sha256(id-signature-input)) + */ + let mut id_signature_input = b"discovery v5 identity proof".to_vec(); + id_signature_input.extend_from_slice(challenge_data); + id_signature_input.extend_from_slice(ephemeral_pubkey); + id_signature_input.extend_from_slice(node_id_b.as_bytes()); + + let digest = Sha256::digest(&id_signature_input); + let message = SecpMessage::from_digest_slice(&digest).expect("32 byte digest"); + SECP256K1.sign_ecdsa(&message, static_key) +} + +/// Creates a secret through elliptic-curve Diffie-Hellman key agreement +/// +/// ecdh(pubkey, privkey) from the spec +/// +/// https://github.com/ethereum/devp2p/blob/master/discv5/discv5-theory.md#identity-specific-cryptography-in-the-handshake +fn compressed_shared_secret(dest_pubkey: &PublicKey, ephemeral_key: &SecretKey) -> [u8; 33] { + let xy_point = shared_secret_point(dest_pubkey, ephemeral_key); + let mut compressed = [0u8; 33]; + let y = &xy_point[32..]; + compressed[0] = if y[31] & 1 == 0 { 0x02 } else { 0x03 }; + compressed[1..].copy_from_slice(&xy_point[..32]); + compressed +} + +#[cfg(test)] +mod tests { + use crate::discv5::codec::Discv5Codec; + + use super::*; + use hex_literal::hex; + use rand::{SeedableRng, rngs::StdRng}; + + #[test] + fn derivation_matches_vector() { + let ephemeral_key = SecretKey::from_byte_array(&hex!( + "fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736" + )) + .unwrap(); + let dest_pubkey = PublicKey::from_slice(&hex!( + "0317931e6e0840220642f230037d285d122bc59063221ef3226b1f403ddc69ca91" + )) + .unwrap(); + let node_id_a = H256::from_slice(&hex!( + "aaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb" + )); + let node_id_b = H256::from_slice(&hex!( + "bbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9" + )); + let challenge_data = hex!( + "000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000" + ); + + let keys = derive_session_keys( + &ephemeral_key, + &dest_pubkey, + &node_id_a, + &node_id_b, + &challenge_data, + ); + assert_eq!(keys.initiator_key, hex!("dccc82d81bd610f4f76d3ebe97a40571")); + assert_eq!(keys.recipient_key, hex!("ac74bb8773749920b0d3a8881c173ec5")); + } + + #[test] + fn id_signature_matches_vector() { + let static_key = SecretKey::from_byte_array(&hex!( + "fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736" + )) + .unwrap(); + let challenge_data = hex!( + "000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000" + ); + let ephemeral_pubkey = + hex!("039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231"); + let node_id_b = H256::from_slice(&hex!( + "bbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9" + )); + + let signature = + create_id_signature(&static_key, &challenge_data, &ephemeral_pubkey, &node_id_b); + assert_eq!( + signature.serialize_compact(), + hex!( + "94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6" + ) + ); + } + + #[test] + fn test_next_nonce_counter() { + let mut codec = Discv5Codec::new(H256::zero()); + + let mut rng = StdRng::seed_from_u64(7); + + let n1 = codec.next_nonce(&mut rng); + let n2 = codec.next_nonce(&mut rng); + + assert_eq!(&n1[..4], &[0, 0, 0, 0]); + assert_eq!(&n2[..4], &[0, 0, 0, 1]); + assert_ne!(&n1[4..], &n2[4..]); + } +} From 16da2acd2b6555327739c8f036d486386f624ba0 Mon Sep 17 00:00:00 2001 From: Edgar Luque Date: Thu, 18 Dec 2025 16:07:22 +0100 Subject: [PATCH 23/29] rename feature --- cmd/ethrex/Cargo.toml | 2 +- crates/networking/p2p/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/ethrex/Cargo.toml b/cmd/ethrex/Cargo.toml index f37c8fba1d9..e310845de6a 100644 --- a/cmd/ethrex/Cargo.toml +++ b/cmd/ethrex/Cargo.toml @@ -102,7 +102,7 @@ jemalloc_profiling = [ ] sync-test = ["ethrex-p2p/sync-test"] # discv5 is currently experimental and should only be enabled for development purposes -discv5 = ["ethrex-p2p/discv5"] +experimental-discv5 = ["ethrex-p2p/experimental-discv5"] l2 = [ "ethrex-l2", diff --git a/crates/networking/p2p/Cargo.toml b/crates/networking/p2p/Cargo.toml index e53f51baae9..a110d41789d 100644 --- a/crates/networking/p2p/Cargo.toml +++ b/crates/networking/p2p/Cargo.toml @@ -69,7 +69,7 @@ l2 = ["dep:ethrex-storage-rollup"] test-utils = [] metrics = ["dep:ethrex-metrics"] # discv5 is currently experimental and should only be enabled for development purposes -discv5 = [] +experimental-discv5 = [] [lints.clippy] unwrap_used = "deny" From de865e2b43360310e68e535f4f8ced3c50684e59 Mon Sep 17 00:00:00 2001 From: Edgar Luque Date: Thu, 18 Dec 2025 16:09:52 +0100 Subject: [PATCH 24/29] rename --- crates/networking/p2p/discv5/messages.rs | 10 +++++----- crates/networking/p2p/p2p.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index a46685d1bf1..019018b7fb0 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -41,7 +41,7 @@ pub enum PacketCodecError { #[error("Invalid protocol: {0}")] InvalidProtocol(String), #[error("Stream Cipher Error: {0}")] - ChipherError(String), + CipherError(String), #[error("TryFromSliceError: {0}")] TryFromSliceError(#[from] TryFromSliceError), #[error("Io Error: {0}")] @@ -50,7 +50,7 @@ pub enum PacketCodecError { impl From for PacketCodecError { fn from(error: StreamCipherError) -> Self { - PacketCodecError::ChipherError(error.to_string()) + PacketCodecError::CipherError(error.to_string()) } } @@ -242,7 +242,7 @@ impl Ordinary { let mut cipher = Aes128Gcm::new(encrypt_key[..16].into()); cipher .encrypt_in_place(nonce.into(), &message_ad, &mut message) - .map_err(|e| PacketCodecError::ChipherError(e.to_string()))?; + .map_err(|e| PacketCodecError::CipherError(e.to_string()))?; Ok((static_header, authdata, message)) } @@ -287,7 +287,7 @@ impl Ordinary { let mut cipher = Aes128Gcm::new(key[..16].into()); cipher .decrypt_in_place(nonce.as_slice().into(), &message_ad, message) - .map_err(|e| PacketCodecError::ChipherError(e.to_string()))?; + .map_err(|e| PacketCodecError::CipherError(e.to_string()))?; Ok(()) } } @@ -406,7 +406,7 @@ impl Handshake { let mut cipher = Aes128Gcm::new(encrypt_key[..16].into()); cipher .encrypt_in_place(nonce.into(), &message_ad, &mut message) - .map_err(|e| PacketCodecError::ChipherError(e.to_string()))?; + .map_err(|e| PacketCodecError::CipherError(e.to_string()))?; Ok((static_header, authdata, message)) } diff --git a/crates/networking/p2p/p2p.rs b/crates/networking/p2p/p2p.rs index b71b2f98230..c2ca66b6592 100644 --- a/crates/networking/p2p/p2p.rs +++ b/crates/networking/p2p/p2p.rs @@ -1,5 +1,5 @@ pub mod discv4; -#[cfg(feature = "discv5")] +#[cfg(feature = "experimental-discv5")] pub mod discv5; pub(crate) mod metrics; pub mod network; From a43b915a565a320b87c3c24a3755985816c52775 Mon Sep 17 00:00:00 2001 From: Edgar Luque Date: Fri, 19 Dec 2025 08:17:08 +0100 Subject: [PATCH 25/29] address comments --- cmd/ethrex/ethrex.rs | 3 +++ crates/networking/p2p/Cargo.toml | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/ethrex/ethrex.rs b/cmd/ethrex/ethrex.rs index 96851dbe26c..1f76bce2999 100644 --- a/cmd/ethrex/ethrex.rs +++ b/cmd/ethrex/ethrex.rs @@ -140,6 +140,9 @@ async fn main() -> eyre::Result<()> { info!("ethrex version: {}", get_client_version()); tokio::spawn(periodically_check_version_update()); + #[cfg(feature = "experimental-discv5")] + tracing::warn!("Experimental Discovery V5 protocol enabled"); + let (datadir, cancel_token, peer_table, local_node_record) = init_l1(opts, Some(log_filter_handler)).await?; diff --git a/crates/networking/p2p/Cargo.toml b/crates/networking/p2p/Cargo.toml index a110d41789d..a5665f01d40 100644 --- a/crates/networking/p2p/Cargo.toml +++ b/crates/networking/p2p/Cargo.toml @@ -46,11 +46,12 @@ serde_json = "1.0.117" concat-kdf = "0.1.0" hmac = "0.12.1" aes = "0.8.4" -aes-gcm = "0.10.3" ctr = "0.9.2" rand = "0.8.5" -hkdf = "0.12.4" +# discv5 +aes-gcm = { version = "0.10.3", optional = true } +hkdf = { version = "0.12.4", optional = true } rayon = "1.10.0" crossbeam.workspace = true @@ -69,7 +70,7 @@ l2 = ["dep:ethrex-storage-rollup"] test-utils = [] metrics = ["dep:ethrex-metrics"] # discv5 is currently experimental and should only be enabled for development purposes -experimental-discv5 = [] +experimental-discv5 = ["dep:aes-gcm", "dep:hkdf"] [lints.clippy] unwrap_used = "deny" From d7ada2b3fc9b8aa5c2cd0505aebc30ad3fb5fccd Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Fri, 19 Dec 2025 12:52:45 -0300 Subject: [PATCH 26/29] Updated some types --- crates/networking/p2p/discv5/messages.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 019018b7fb0..8a95f763732 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -65,7 +65,7 @@ pub enum Packet { pub struct PacketHeader { pub static_header: Vec, pub flag: u8, - pub nonce: Vec, + pub nonce: [u8; 12], pub authdata: Vec, /// Offset in the encoded packet where authdata ends, i.e where the header ends. pub header_end_offset: usize, @@ -74,7 +74,7 @@ pub struct PacketHeader { impl Packet { pub fn decode( dest_id: &H256, - decrypt_key: &[u8], + decrypt_key: &[u8; 16], encoded_packet: &[u8], ) -> Result { if encoded_packet.len() < MIN_PACKET_SIZE || encoded_packet.len() > MAX_PACKET_SIZE { @@ -117,7 +117,7 @@ impl Packet { &self, buf: &mut dyn BufMut, masking_iv: u128, - nonce: &[u8], + nonce: &[u8; 12], dest_id: &H256, encrypt_key: &[u8], ) -> Result<(), PacketCodecError> { @@ -552,7 +552,7 @@ impl Message { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PingMessage { /// The request id of the sender. - pub req_id: Vec, + pub req_id: u64, /// The ENR sequence number of the sender. pub enr_seq: u64, } @@ -566,7 +566,7 @@ impl PingMessage { impl RLPEncode for PingMessage { fn encode(&self, buf: &mut dyn BufMut) { Encoder::new(buf) - .encode_field(&Bytes::from(self.req_id.clone())) + .encode_field(self.req_id.as_slice()) .encode_field(&self.enr_seq) .finish(); } @@ -574,7 +574,7 @@ impl RLPEncode for PingMessage { impl RLPDecode for PingMessage { fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { - let ((req_id, enr_seq), remaining): ((Bytes, u64), &[u8]) = + let ((req_id, enr_seq), remaining): ((&[u8], u64), &[u8]) = RLPDecode::decode_unfinished(rlp)?; let ping = PingMessage { req_id: req_id.to_vec(), From 2aca677fe75c46de166c46db51d556631507e0da Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Fri, 19 Dec 2025 16:09:04 -0300 Subject: [PATCH 27/29] Addressed PR comments and corrected req_id type --- crates/networking/p2p/discv5/codec.rs | 4 +- crates/networking/p2p/discv5/messages.rs | 106 ++++++++++++----------- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/crates/networking/p2p/discv5/codec.rs b/crates/networking/p2p/discv5/codec.rs index 33bd83ca3fb..083ff824884 100644 --- a/crates/networking/p2p/discv5/codec.rs +++ b/crates/networking/p2p/discv5/codec.rs @@ -56,9 +56,9 @@ impl Decoder for Discv5Codec { fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { if !buf.is_empty() { - let key: &[u8] = match &self.session { + let key: &[u8; 16] = match &self.session { Some(session) => session.inbound_key(), - None => &[], + None => &[0; 16], }; Ok(Some(Packet::decode( &self.dest_id, diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 8a95f763732..bf64db18dd8 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -96,7 +96,7 @@ impl Packet { masking_iv, packet_header.static_header, packet_header.authdata, - packet_header.nonce, + &packet_header.nonce, decrypt_key, encrypted_message, )?)), @@ -130,7 +130,7 @@ impl Packet { match self { Packet::Ordinary(ordinary) => { let (mut static_header, mut authdata, encrypted_message) = - ordinary.encode(&nonce, &masking_as_bytes, encrypt_key)?; + ordinary.encode(nonce, &masking_as_bytes, encrypt_key)?; cipher.try_apply_keystream(&mut static_header)?; buf.put_slice(&static_header); @@ -143,7 +143,7 @@ impl Packet { } Packet::Handshake(handshake) => { let (mut static_header, mut authdata, encrypted_message) = - handshake.encode(&nonce, &masking_as_bytes, encrypt_key)?; + handshake.encode(nonce, &masking_as_bytes, encrypt_key)?; cipher.try_apply_keystream(&mut static_header)?; buf.put_slice(&static_header); @@ -178,7 +178,7 @@ impl Packet { } let flag = static_header[8]; - let nonce = static_header[9..21].to_vec(); + let nonce = static_header[9..21].try_into()?; let authdata_size = u16::from_be_bytes(static_header[21..23].try_into()?) as usize; let authdata_end = STATIC_HEADER_END + authdata_size; let authdata = &mut encoded_packet[STATIC_HEADER_END..authdata_end].to_vec(); @@ -211,7 +211,7 @@ impl Ordinary { #[allow(clippy::type_complexity)] fn encode( &self, - nonce: &[u8], + nonce: &[u8; 12], masking_iv: &[u8], encrypt_key: &[u8], ) -> Result<(Vec, Vec, Vec), PacketCodecError> { @@ -251,7 +251,7 @@ impl Ordinary { masking_iv: &[u8], static_header: Vec, authdata: Vec, - nonce: Vec, + nonce: &[u8; 12], decrypt_key: &[u8], encrypted_message: &[u8], ) -> Result { @@ -280,7 +280,7 @@ impl Ordinary { fn decrypt( key: &[u8], - nonce: Vec, + nonce: &[u8; 12], message: &mut Vec, message_ad: Vec, ) -> Result<(), PacketCodecError> { @@ -375,7 +375,7 @@ impl Handshake { #[allow(clippy::type_complexity)] fn encode( &self, - nonce: &[u8], + nonce: &[u8; 12], masking_iv: &[u8], encrypt_key: &[u8], ) -> Result<(Vec, Vec, Vec), PacketCodecError> { @@ -462,7 +462,7 @@ impl Handshake { message_ad.extend_from_slice(&authdata); let mut message = encrypted_message.to_vec(); - Ordinary::decrypt(decrypt_key, nonce, &mut message, message_ad)?; + Ordinary::decrypt(decrypt_key, &nonce, &mut message, message_ad)?; let message = Message::decode(&message)?; Ok(Handshake { @@ -552,13 +552,13 @@ impl Message { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PingMessage { /// The request id of the sender. - pub req_id: u64, + pub req_id: Bytes, /// The ENR sequence number of the sender. pub enr_seq: u64, } impl PingMessage { - pub fn new(req_id: Vec, enr_seq: u64) -> Self { + pub fn new(req_id: Bytes, enr_seq: u64) -> Self { Self { req_id, enr_seq } } } @@ -566,7 +566,7 @@ impl PingMessage { impl RLPEncode for PingMessage { fn encode(&self, buf: &mut dyn BufMut) { Encoder::new(buf) - .encode_field(self.req_id.as_slice()) + .encode_field(&self.req_id) .encode_field(&self.enr_seq) .finish(); } @@ -574,19 +574,22 @@ impl RLPEncode for PingMessage { impl RLPDecode for PingMessage { fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { - let ((req_id, enr_seq), remaining): ((&[u8], u64), &[u8]) = - RLPDecode::decode_unfinished(rlp)?; + let decoder = Decoder::new(rlp)?; + // let (req_id, decoder): (Bytes, Decoder) = decoder.decode_field("req_id")?; + let (req_id, decoder) = decoder.decode_field("req_id")?; + let (enr_seq, decoder) = decoder.decode_field("enr_seq")?; let ping = PingMessage { - req_id: req_id.to_vec(), + // req_id: req_id.to_vec(), + req_id: req_id, enr_seq, }; - Ok((ping, remaining)) + Ok((ping, decoder.finish()?)) } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PongMessage { - pub req_id: u64, + pub req_id: Bytes, pub enr_seq: u64, pub recipient_addr: IpAddr, } @@ -621,7 +624,7 @@ impl RLPDecode for PongMessage { #[derive(Debug, Clone, PartialEq, Eq)] pub struct FindNodeMessage { - pub req_id: u64, + pub req_id: Bytes, pub distance: Vec, } @@ -646,7 +649,7 @@ impl RLPDecode for FindNodeMessage { #[derive(Debug, Clone, PartialEq, Eq)] pub struct NodesMessage { - pub req_id: u64, + pub req_id: Bytes, pub total: u64, pub nodes: Vec, } @@ -681,7 +684,7 @@ impl RLPDecode for NodesMessage { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TalkReqMessage { - pub req_id: u64, + pub req_id: Bytes, pub protocol: Bytes, pub request: Bytes, } @@ -716,7 +719,7 @@ impl RLPDecode for TalkReqMessage { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TalkResMessage { - pub req_id: u64, + pub req_id: Bytes, pub response: Vec, } @@ -731,7 +734,8 @@ impl RLPEncode for TalkResMessage { impl RLPDecode for TalkResMessage { fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { - let ((req_id, response), remaining) = <(u64, Bytes) as RLPDecode>::decode_unfinished(rlp)?; + let ((req_id, response), remaining) = + <(Bytes, Bytes) as RLPDecode>::decode_unfinished(rlp)?; Ok(( Self { @@ -745,7 +749,7 @@ impl RLPDecode for TalkResMessage { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TicketMessage { - pub req_id: u64, + pub req_id: Bytes, pub ticket: Bytes, pub wait_time: u64, } @@ -809,7 +813,7 @@ mod tests { // .unwrap(); #[test] - fn test_aes_gcm_vector() { + fn aes_gcm_vector() { // https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md#encryptiondecryption let key = hex!("9f2d77db7004bf8a1a85107ac686990b"); let nonce = hex!("27b5af763c446acd2749fe8e"); @@ -847,13 +851,13 @@ mod tests { eph_pubkey: vec![2; 33], record: None, message: Message::Ping(PingMessage { - req_id: vec![3], + req_id: Bytes::from_static(&[3]), enr_seq: 4, }), }; - let key = vec![0x10; 16]; - let nonce = hex!("000102030405060708090a0b").to_vec(); + let key = [0x10; 16]; + let nonce = hex!("000102030405060708090a0b"); let mut buf = Vec::new(); let packet = Packet::Handshake(handshake.clone()); packet.encode(&mut buf, 0, &nonce, &dest_id, &key).unwrap(); @@ -864,7 +868,7 @@ mod tests { /// Ping handshake packet (flag 2) from https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md #[test] - fn handshake_packet_vector_test_roundtrip() { + fn handshake_packet_vector_roundtrip() { /* # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 @@ -899,7 +903,7 @@ mod tests { let encoded = &hex!( "00000000000000000000000000000000088b3d4342774649305f313964a39e55ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfba776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d839cf8" ); - let read_key = hex!("4f9fac6de7567d1e3b1241dffe90f662").to_vec(); + let read_key = hex!("4f9fac6de7567d1e3b1241dffe90f662"); let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); let handshake = match packet { @@ -921,13 +925,13 @@ mod tests { assert_eq!( handshake.message, Message::Ping(PingMessage { - req_id: hex!("00000001").to_vec(), + req_id: Bytes::from(hex!("00000001").as_slice()), enr_seq: 1, }) ); let masking_iv = u128::from_be_bytes(encoded[..16].try_into().unwrap()); - let nonce = hex!("ffffffffffffffffffffffff").to_vec(); + let nonce = hex!("ffffffffffffffffffffffff"); let mut buf = Vec::new(); Packet::Handshake(handshake) .encode(&mut buf, masking_iv, &nonce, &dest_id, &read_key) @@ -938,7 +942,7 @@ mod tests { /// Ping handshake message packet (flag 2, with ENR) from https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md #[test] - fn handshake_packet_with_enr_vector_test_roundtrip() { + fn handshake_packet_with_enr_vector_roundtrip() { let node_b_key = SecretKey::from_byte_array(&hex!( "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" )) @@ -948,8 +952,8 @@ mod tests { let encoded = &hex!( "00000000000000000000000000000000088b3d4342774649305f313964a39e55ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be98562fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b21481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb12a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b139471" ); - let nonce = hex!("ffffffffffffffffffffffff").to_vec(); - let read_key = hex!("53b1c075f41876423154e157470c2f48").to_vec(); + let nonce = hex!("ffffffffffffffffffffffff"); + let read_key = hex!("53b1c075f41876423154e157470c2f48"); let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); let handshake = match packet { @@ -970,7 +974,7 @@ mod tests { assert_eq!( handshake.message, Message::Ping(PingMessage { - req_id: hex!("00000001").to_vec(), + req_id: Bytes::from(hex!("00000001").as_slice()), enr_seq: 1, }) ); @@ -990,7 +994,7 @@ mod tests { } #[test] - fn test_encode_whoareyou_packet() { + fn encode_whoareyou_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb // # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 // # whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000 @@ -1033,7 +1037,7 @@ mod tests { } #[test] - fn test_decode_whoareyou_packet() { + fn decode_whoareyou_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb // # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 // # whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000 @@ -1069,7 +1073,7 @@ mod tests { } #[test] - fn test_decode_ping_packet() { + fn decode_ping_packet() { // # src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb // # dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9 // # nonce = 0xffffffffffffffffffffffff @@ -1097,12 +1101,12 @@ mod tests { "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" ); // # read-key = 0x00000000000000000000000000000000 - let read_key = [0; 16].to_vec(); + let read_key = [0; 16]; let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); let expected = Packet::Ordinary(Ordinary { src_id, message: Message::Ping(PingMessage { - req_id: hex!("00000001").to_vec(), + req_id: Bytes::from(hex!("00000001").as_slice()), enr_seq: 2, }), }); @@ -1112,7 +1116,7 @@ mod tests { /// Ping message packet (flag 0) from https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md #[test] - fn ordinary_ping_packet_vector_test_roundtrip() { + fn ordinary_ping_packet_vector_roundtrip() { let node_b_key = SecretKey::from_byte_array(&hex!( "66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628" )) @@ -1122,8 +1126,8 @@ mod tests { let encoded = &hex!( "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" ); - let nonce = hex!("ffffffffffffffffffffffff").to_vec(); - let read_key = [0; 16].to_vec(); + let nonce = hex!("ffffffffffffffffffffffff"); + let read_key = [0; 16]; let packet = Packet::decode(&dest_id, &read_key, encoded).unwrap(); let expected = Packet::Ordinary(Ordinary { @@ -1131,7 +1135,7 @@ mod tests { "aaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb" )), message: Message::Ping(PingMessage { - req_id: hex!("00000001").to_vec(), + req_id: Bytes::from(hex!("00000001").as_slice()), enr_seq: 2, }), }); @@ -1148,7 +1152,7 @@ mod tests { #[test] fn ping_packet_codec_roundtrip() { let pkt = PingMessage { - req_id: [1, 2, 3, 4].to_vec(), + req_id: Bytes::from_static(&[1, 2, 3, 4]), enr_seq: 4321, }; @@ -1161,7 +1165,7 @@ mod tests { #[test] fn pong_packet_codec_roundtrip() { let pkt = PongMessage { - req_id: 1234, + req_id: Bytes::from_static(&[1, 2, 3, 4]), enr_seq: 4321, recipient_addr: Ipv4Addr::BROADCAST.into(), }; @@ -1173,7 +1177,7 @@ mod tests { #[test] fn findnode_packet_codec_roundtrip() { let pkt = FindNodeMessage { - req_id: 1234, + req_id: Bytes::from_static(&[1, 2, 3, 4]), distance: vec![1, 2, 3, 4], }; @@ -1190,7 +1194,7 @@ mod tests { .into(); let pkt = NodesMessage { - req_id: 1234, + req_id: Bytes::from_static(&[1, 2, 3, 4]), total: 2, nodes: vec![NodeRecord { seq: 4321, @@ -1206,7 +1210,7 @@ mod tests { #[test] fn talkreq_packet_codec_roundtrip() { let pkt = TalkReqMessage { - req_id: 1234, + req_id: Bytes::from_static(&[1, 2, 3, 4]), protocol: Bytes::from_static(&[1, 2, 3, 4]), request: Bytes::from_static(&[1, 2, 3, 4]), }; @@ -1218,7 +1222,7 @@ mod tests { #[test] fn talk_res_packet_codec_roundtrip() { let pkt = TalkResMessage { - req_id: 1234, + req_id: Bytes::from_static(&[1, 2, 3, 4]), response: b"\x00\x01\x02\x03".into(), }; @@ -1229,7 +1233,7 @@ mod tests { #[test] fn ticket_packet_codec_roundtrip() { let pkt = TicketMessage { - req_id: 1234, + req_id: Bytes::from_static(&[1, 2, 3, 4]), ticket: Bytes::from_static(&[1, 2, 3, 4]), wait_time: 5, }; From 6b819a1a8ce948b6ddfd68c27481d12bd3a3a15c Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Fri, 19 Dec 2025 16:24:59 -0300 Subject: [PATCH 28/29] Corrected static_header type --- crates/networking/p2p/discv5/messages.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index bf64db18dd8..2f86aa79547 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -27,8 +27,9 @@ const PROTOCOL_ID: &[u8] = b"discv5"; const PROTOCOL_VERSION: u16 = 0x0001; // masking-iv size for a u128 const IV_MASKING_SIZE: usize = 16; -// static_header end limit: 23 bytes from static_header + 16 from iv_masking -const STATIC_HEADER_END: usize = IV_MASKING_SIZE + 23; +// static_header size is 23 bytes +const STATIC_HEADER_SIZE: usize = 23; +const STATIC_HEADER_END: usize = IV_MASKING_SIZE + STATIC_HEADER_SIZE; #[derive(Debug, thiserror::Error)] pub enum PacketCodecError { @@ -63,7 +64,7 @@ pub enum Packet { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PacketHeader { - pub static_header: Vec, + pub static_header: [u8; STATIC_HEADER_SIZE], pub flag: u8, pub nonce: [u8; 12], pub authdata: Vec, @@ -94,7 +95,7 @@ impl Packet { match packet_header.flag { 0x00 => Ok(Packet::Ordinary(Ordinary::decode( masking_iv, - packet_header.static_header, + &packet_header.static_header, packet_header.authdata, &packet_header.nonce, decrypt_key, @@ -160,7 +161,8 @@ impl Packet { encoded_packet: &[u8], ) -> Result { // static header - let mut static_header = encoded_packet[IV_MASKING_SIZE..STATIC_HEADER_END].to_vec(); + let mut static_header: [u8; STATIC_HEADER_SIZE] = + encoded_packet[IV_MASKING_SIZE..STATIC_HEADER_END].try_into()?; cipher.try_apply_keystream(&mut static_header)?; @@ -249,7 +251,7 @@ impl Ordinary { pub fn decode( masking_iv: &[u8], - static_header: Vec, + static_header: &[u8; STATIC_HEADER_SIZE], authdata: Vec, nonce: &[u8; 12], decrypt_key: &[u8], @@ -266,7 +268,7 @@ impl Ordinary { // message-pt = message-type || message-data // message-ad = masking-iv || header let mut message_ad = masking_iv.to_vec(); - message_ad.extend_from_slice(&static_header); + message_ad.extend_from_slice(&static_header.as_slice()); message_ad.extend_from_slice(&authdata); let mut message = encrypted_message.to_vec(); From 41d100e4114bf258ca5214a8f604a61369d99429 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Fri, 19 Dec 2025 16:28:14 -0300 Subject: [PATCH 29/29] Removed unnecessary stuff --- crates/networking/p2p/discv5/messages.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/networking/p2p/discv5/messages.rs b/crates/networking/p2p/discv5/messages.rs index 2f86aa79547..507b7b0135d 100644 --- a/crates/networking/p2p/discv5/messages.rs +++ b/crates/networking/p2p/discv5/messages.rs @@ -268,7 +268,7 @@ impl Ordinary { // message-pt = message-type || message-data // message-ad = masking-iv || header let mut message_ad = masking_iv.to_vec(); - message_ad.extend_from_slice(&static_header.as_slice()); + message_ad.extend_from_slice(static_header.as_slice()); message_ad.extend_from_slice(&authdata); let mut message = encrypted_message.to_vec(); @@ -577,14 +577,9 @@ impl RLPEncode for PingMessage { impl RLPDecode for PingMessage { fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { let decoder = Decoder::new(rlp)?; - // let (req_id, decoder): (Bytes, Decoder) = decoder.decode_field("req_id")?; let (req_id, decoder) = decoder.decode_field("req_id")?; let (enr_seq, decoder) = decoder.decode_field("enr_seq")?; - let ping = PingMessage { - // req_id: req_id.to_vec(), - req_id: req_id, - enr_seq, - }; + let ping = PingMessage { req_id, enr_seq }; Ok((ping, decoder.finish()?)) } }