diff --git a/Cargo.lock b/Cargo.lock index 0f91f52627..fe4ac3f292 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", ] @@ -3882,6 +3907,7 @@ name = "ethrex-p2p" version = "8.0.0" dependencies = [ "aes", + "aes-gcm", "async-trait", "bytes", "concat-kdf", @@ -3900,6 +3926,7 @@ dependencies = [ "futures", "hex", "hex-literal 0.4.1", + "hkdf", "hmac", "indexmap 2.12.1", "lazy_static", @@ -4645,6 +4672,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" @@ -5176,6 +5213,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" @@ -7266,6 +7312,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" @@ -9502,6 +9554,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" @@ -13767,6 +13831,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/cmd/ethrex/Cargo.toml b/cmd/ethrex/Cargo.toml index be18f8ffd2..02aac989b7 100644 --- a/cmd/ethrex/Cargo.toml +++ b/cmd/ethrex/Cargo.toml @@ -102,6 +102,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 +experimental-discv5 = ["ethrex-p2p/experimental-discv5"] l2 = [ "ethrex-l2", diff --git a/cmd/ethrex/ethrex.rs b/cmd/ethrex/ethrex.rs index 348c0b279d..106ae70ebc 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/common/types/transaction.rs b/crates/common/types/transaction.rs index 6e38ed04a3..49aa761d73 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, +} + +impl Discv5Codec { + pub fn new(dest_id: H256) -> Self { + Self { + dest_id, + counter: 0, + session: None, + } + } + + 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 = PacketCodecError; + + fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { + if !buf.is_empty() { + let key: &[u8; 16] = match &self.session { + Some(session) => session.inbound_key(), + None => &[0; 16], + }; + Ok(Some(Packet::decode( + &self.dest_id, + key, + &buf.split_to(buf.len()), + )?)) + } else { + Ok(None) + } + } +} + +impl Encoder for Discv5Codec { + type Error = PacketCodecError; + + fn encode(&mut self, packet: Packet, buf: &mut BytesMut) -> Result<(), Self::Error> { + let masking_iv: u128 = rand::random(); + 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 new file mode 100644 index 0000000000..507b7b0135 --- /dev/null +++ b/crates/networking/p2p/discv5/messages.rs @@ -0,0 +1,1241 @@ +use std::{array::TryFromSliceError, net::IpAddr}; + +use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherError}; +use aes_gcm::{Aes128Gcm, KeyInit, aead::AeadMutInPlace}; +use bytes::{BufMut, Bytes}; +use ethrex_common::H256; +use ethrex_rlp::{ + decode::RLPDecode, + encode::RLPEncode, + error::RLPDecodeError, + structs::{Decoder, Encoder}, +}; + +use crate::types::NodeRecord; + +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; +/// 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; +// masking-iv size for a u128 +const IV_MASKING_SIZE: usize = 16; +// 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 { + #[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}")] + CipherError(String), + #[error("TryFromSliceError: {0}")] + TryFromSliceError(#[from] TryFromSliceError), + #[error("Io Error: {0}")] + IoError(#[from] std::io::Error), +} + +impl From for PacketCodecError { + fn from(error: StreamCipherError) -> Self { + PacketCodecError::CipherError(error.to_string()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Packet { + Ordinary(Ordinary), + WhoAreYou(WhoAreYou), + Handshake(Handshake), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PacketHeader { + pub static_header: [u8; STATIC_HEADER_SIZE], + pub flag: u8, + 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, +} + +impl Packet { + pub fn decode( + dest_id: &H256, + decrypt_key: &[u8; 16], + encoded_packet: &[u8], + ) -> Result { + if encoded_packet.len() < MIN_PACKET_SIZE || encoded_packet.len() > MAX_PACKET_SIZE { + return Err(PacketCodecError::InvalidSize); + } + + // the packet structure is + // masking-iv || masked-header || message + // 16 bytes for an u128 + let masking_iv = &encoded_packet[..IV_MASKING_SIZE]; + + 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( + masking_iv, + &packet_header.static_header, + packet_header.authdata, + &packet_header.nonce, + decrypt_key, + 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)?, + } + } + + pub fn encode( + &self, + buf: &mut dyn BufMut, + masking_iv: u128, + nonce: &[u8; 12], + dest_id: &H256, + encrypt_key: &[u8], + ) -> Result<(), PacketCodecError> { + let masking_as_bytes = masking_iv.to_be_bytes(); + buf.put_slice(&masking_as_bytes); + + let mut cipher = + ::new(dest_id[..16].into(), masking_as_bytes[..].into()); + + match self { + 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)?; + } + 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(()) + } + + fn decode_header( + cipher: &mut T, + encoded_packet: &[u8], + ) -> Result { + // static header + 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)?; + + // static-header = protocol-id || version || flag || nonce || authdata-size + //protocol check + 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(PacketCodecError::InvalidProtocol( + match str::from_utf8(protocol_id) { + Ok(result) => format!("{} v{}", result, version), + Err(_) => format!("{:?} v{}", protocol_id, version), + }, + )); + } + + let flag = static_header[8]; + 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(); + + cipher.try_apply_keystream(authdata)?; + + Ok(PacketHeader { + static_header, + flag, + nonce, + authdata: authdata.to_vec(), + header_end_offset: authdata_end, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Ordinary { + pub src_id: H256, + pub message: Message, +} + +impl Ordinary { + fn encode_authdata(&self, buf: &mut dyn BufMut) -> Result<(), PacketCodecError> { + 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; 12], + masking_iv: &[u8], + encrypt_key: &[u8], + ) -> Result<(Vec, Vec, Vec), PacketCodecError> { + if encrypt_key.len() < 16 { + 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(|_| PacketCodecError::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| PacketCodecError::CipherError(e.to_string()))?; + + Ok((static_header, authdata, message)) + } + + pub fn decode( + masking_iv: &[u8], + static_header: &[u8; STATIC_HEADER_SIZE], + authdata: Vec, + nonce: &[u8; 12], + decrypt_key: &[u8], + encrypted_message: &[u8], + ) -> Result { + if authdata.len() != 32 { + return Err(PacketCodecError::InvalidSize); + } + if decrypt_key.len() < 16 { + return Err(PacketCodecError::InvalidSize); + } + + // 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.as_slice()); + message_ad.extend_from_slice(&authdata); + + 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 { src_id, message }) + } + + fn decrypt( + key: &[u8], + nonce: &[u8; 12], + message: &mut Vec, + message_ad: Vec, + ) -> Result<(), PacketCodecError> { + let mut cipher = Aes128Gcm::new(key[..16].into()); + cipher + .decrypt_in_place(nonce.as_slice().into(), &message_ad, message) + .map_err(|e| PacketCodecError::CipherError(e.to_string()))?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WhoAreYou { + pub id_nonce: u128, + pub enr_seq: u64, +} + +impl WhoAreYou { + fn encode_header( + &self, + buf: &mut dyn BufMut, + cipher: &mut T, + nonce: &[u8], + ) -> Result<(), PacketCodecError> { + 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: &[u8]) -> 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 }) + } +} + +#[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<(), PacketCodecError> { + let sig_size: u8 = self + .id_signature + .len() + .try_into() + .map_err(|_| PacketCodecError::InvalidSize)?; + let eph_key_size: u8 = self + .eph_pubkey + .len() + .try_into() + .map_err(|_| PacketCodecError::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; 12], + masking_iv: &[u8], + encrypt_key: &[u8], + ) -> 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(|_| PacketCodecError::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(PacketCodecError::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| PacketCodecError::CipherError(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 { + if decrypt_key.len() < 16 { + return Err(PacketCodecError::InvalidSize); + } + let PacketHeader { + static_header, + nonce, + authdata, + .. + } = header; + + if authdata.len() < HANDSHAKE_AUTHDATA_HEAD { + return Err(PacketCodecError::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(PacketCodecError::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), + Pong(PongMessage), + FindNode(FindNodeMessage), + Nodes(NodesMessage), + TalkReq(TalkReqMessage), + TalkRes(TalkResMessage), + Ticket(TicketMessage), + // TODO: add the other messages +} + +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 { + 0x01 => { + let ping = PingMessage::decode(&encrypted_message[1..])?; + Ok(Message::Ping(ping)) + } + 0x02 => { + let pong = PongMessage::decode(&encrypted_message[1..])?; + Ok(Message::Pong(pong)) + } + 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)) + } + 0x05 => { + let talk_req_msg = TalkReqMessage::decode(&encrypted_message[1..])?; + Ok(Message::TalkReq(talk_req_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)) + } + _ => Err(RLPDecodeError::MalformedData), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PingMessage { + /// The request id of the sender. + pub req_id: Bytes, + /// The ENR sequence number of the sender. + pub enr_seq: u64, +} + +impl PingMessage { + pub fn new(req_id: Bytes, 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 }; + Ok((ping, decoder.finish()?)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PongMessage { + pub req_id: Bytes, + 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()?, + )) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FindNodeMessage { + pub req_id: Bytes, + 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: Bytes, + 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: Bytes, + 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()?, + )) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TalkResMessage { + pub req_id: Bytes, + 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) = + <(Bytes, 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: Bytes, + 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::*; + use crate::{ + discv5::{ + codec::Discv5Codec, + messages::{Message, Ordinary, Packet, PingMessage, WhoAreYou}, + }, + 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; + use secp256k1::SecretKey; + use std::net::Ipv4Addr; + use tokio_util::codec::Decoder as _; + + // 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 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!( + "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: Bytes::from_static(&[3]), + enr_seq: 4, + }), + }; + + 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(); + + 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_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"); + + 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: 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"); + 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()); + } + + /// 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_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"); + let read_key = hex!("53b1c075f41876423154e157470c2f48"); + + 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: Bytes::from(hex!("00000001").as_slice()), + 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 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, + 0, + &hex!("0102030405060708090a0b0c"), + &dest_id, + &[], + ); + let expected = &hex!( + "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" + ); + + assert_eq!(buf, expected); + } + + #[test] + fn 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 mut codec = Discv5Codec::new(dest_id); + + let mut encoded = BytesMut::from(hex!( + "00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d" + ).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() + .try_into() + .unwrap(), + ), + enr_seq: 0, + })); + + assert_eq!(packet, expected); + } + + #[test] + fn 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_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!( + "00000000000000000000000000000000088b3d4342774649325f313964a39e55ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d34c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc" + ); + // # read-key = 0x00000000000000000000000000000000 + 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: Bytes::from(hex!("00000001").as_slice()), + enr_seq: 2, + }), + }); + + 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_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"); + let read_key = [0; 16]; + + 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: Bytes::from(hex!("00000001").as_slice()), + 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 { + req_id: Bytes::from_static(&[1, 2, 3, 4]), + 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] + fn pong_packet_codec_roundtrip() { + let pkt = PongMessage { + req_id: Bytes::from_static(&[1, 2, 3, 4]), + enr_seq: 4321, + recipient_addr: Ipv4Addr::BROADCAST.into(), + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(PongMessage::decode(&buf).unwrap(), pkt); + } + + #[test] + fn findnode_packet_codec_roundtrip() { + let pkt = FindNodeMessage { + req_id: Bytes::from_static(&[1, 2, 3, 4]), + 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 { + id: Some("id".to_string()), + ..Default::default() + } + .into(); + + let pkt = NodesMessage { + req_id: Bytes::from_static(&[1, 2, 3, 4]), + total: 2, + nodes: vec![NodeRecord { + seq: 4321, + 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 { + 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]), + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(TalkReqMessage::decode(&buf).unwrap(), pkt); + } + + #[test] + fn talk_res_packet_codec_roundtrip() { + let pkt = TalkResMessage { + req_id: Bytes::from_static(&[1, 2, 3, 4]), + 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 { + req_id: Bytes::from_static(&[1, 2, 3, 4]), + ticket: Bytes::from_static(&[1, 2, 3, 4]), + wait_time: 5, + }; + + let buf = pkt.encode_to_vec(); + assert_eq!(TicketMessage::decode(&buf).unwrap(), pkt); + } +} diff --git a/crates/networking/p2p/discv5/mod.rs b/crates/networking/p2p/discv5/mod.rs new file mode 100644 index 0000000000..719d9e319f --- /dev/null +++ b/crates/networking/p2p/discv5/mod.rs @@ -0,0 +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 0000000000..5a8f2c7f3a --- /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..]); + } +} diff --git a/crates/networking/p2p/p2p.rs b/crates/networking/p2p/p2p.rs index 43e6b98482..c2ca66b659 100644 --- a/crates/networking/p2p/p2p.rs +++ b/crates/networking/p2p/p2p.rs @@ -1,4 +1,6 @@ pub mod discv4; +#[cfg(feature = "experimental-discv5")] +pub mod discv5; pub(crate) mod metrics; pub mod network; pub mod peer_handler; diff --git a/crates/networking/p2p/rlpx/utils.rs b/crates/networking/p2p/rlpx/utils.rs index ad6c0e0baf..81e8515101 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();