diff --git a/cmd/ethrex/initializers.rs b/cmd/ethrex/initializers.rs index d010cad165f..c6db28ed788 100644 --- a/cmd/ethrex/initializers.rs +++ b/cmd/ethrex/initializers.rs @@ -331,7 +331,7 @@ pub fn get_local_node_record( ) -> NodeRecord { match read_node_config_file(datadir) { Ok(Some(ref mut config)) => { - NodeRecord::from_node(local_p2p_node, config.node_record.seq + 1, signer) + NodeRecord::from_node(local_p2p_node, config.node_record.seq + 1, signer, None) .expect("Node record could not be created from local node") } _ => { @@ -339,7 +339,7 @@ pub fn get_local_node_record( .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); - NodeRecord::from_node(local_p2p_node, timestamp, signer) + NodeRecord::from_node(local_p2p_node, timestamp, signer, None) .expect("Node record could not be created from local node") } } diff --git a/crates/common/types/fork_id.rs b/crates/common/types/fork_id.rs index c7fa3ff40d1..6772dcf81e8 100644 --- a/crates/common/types/fork_id.rs +++ b/crates/common/types/fork_id.rs @@ -7,6 +7,7 @@ use ethrex_rlp::{ }; use ethereum_types::H32; +use serde::{Deserialize, Serialize}; use tracing::debug; use super::{BlockHash, BlockHeader, BlockNumber, ChainConfig}; @@ -14,7 +15,7 @@ use super::{BlockHash, BlockHeader, BlockNumber, ChainConfig}; // See https://github.com/ethereum/go-ethereum/blob/530adfc8e3ef9c8b6356facecdec10b30fb81d7d/core/forkid/forkid.go#L51 const TIMESTAMP_THRESHOLD: u64 = 1438269973; -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct ForkId { pub fork_hash: H32, pub fork_next: BlockNumber, diff --git a/crates/networking/p2p/discv4/messages.rs b/crates/networking/p2p/discv4/messages.rs index 1ccf66bb466..fb679e54c65 100644 --- a/crates/networking/p2p/discv4/messages.rs +++ b/crates/networking/p2p/discv4/messages.rs @@ -525,6 +525,8 @@ impl RLPEncode for ENRResponseMessage { #[cfg(test)] mod tests { + use crate::types::NodeRecordPairs; + use super::*; use bytes::Bytes; use ethrex_common::{H256, H264}; @@ -759,10 +761,11 @@ mod tests { (String::from("tcp").into(), tcp_rlp.clone().into()), (String::from("udp").into(), udp_rlp.clone().into()), ]; + let record_pairs = NodeRecordPairs::decode_pairs(pairs); let node_record = NodeRecord { signature, seq, - pairs, + pairs: record_pairs, }; let msg = Message::ENRResponse(ENRResponseMessage { request_hash, @@ -885,10 +888,13 @@ mod tests { (String::from("tcp").into(), tcp_rlp.clone().into()), (String::from("udp").into(), udp_rlp.clone().into()), ]; + + let record_pairs = NodeRecordPairs::decode_pairs(pairs); + let node_record = NodeRecord { signature, seq, - pairs, + pairs: record_pairs, }; let expected = Message::ENRResponse(ENRResponseMessage { request_hash, diff --git a/crates/networking/p2p/discv4/server.rs b/crates/networking/p2p/discv4/server.rs index 477975ec1d9..21f6ae433c4 100644 --- a/crates/networking/p2p/discv4/server.rs +++ b/crates/networking/p2p/discv4/server.rs @@ -15,6 +15,7 @@ use crate::{ }; use bytes::BytesMut; use ethrex_common::{H256, H512}; +use ethrex_storage::Store; use futures::StreamExt; use rand::rngs::OsRng; use secp256k1::SecretKey; @@ -89,6 +90,7 @@ pub struct DiscoveryServer { impl DiscoveryServer { pub async fn spawn( + storage: Store, local_node: Node, signer: SecretKey, udp_socket: Arc, @@ -97,7 +99,8 @@ impl DiscoveryServer { ) -> Result<(), DiscoveryServerError> { info!("Starting Discovery Server"); - let local_node_record = NodeRecord::from_node(&local_node, 1, &signer) + let fork_id = storage.get_fork_id().await.ok(); + let local_node_record = NodeRecord::from_node(&local_node, 1, &signer, fork_id) .expect("Failed to create local node record"); let mut discovery_server = Self { local_node: local_node.clone(), diff --git a/crates/networking/p2p/network.rs b/crates/networking/p2p/network.rs index b7bd8324313..55574e18d99 100644 --- a/crates/networking/p2p/network.rs +++ b/crates/networking/p2p/network.rs @@ -111,6 +111,7 @@ pub async fn start_network(context: P2PContext, bootnodes: Vec) -> Result< ); DiscoveryServer::spawn( + context.storage.clone(), context.local_node.clone(), context.signer, udp_socket.clone(), diff --git a/crates/networking/p2p/types.rs b/crates/networking/p2p/types.rs index 0196e8834c0..69af76054e6 100644 --- a/crates/networking/p2p/types.rs +++ b/crates/networking/p2p/types.rs @@ -188,10 +188,8 @@ impl Node { } pub fn from_enr_url(enr: &str) -> Result { - let base64_decoded = ethrex_common::base64::decode(&enr.as_bytes()[4..]); - let record = NodeRecord::decode(&base64_decoded).map_err(NodeError::from)?; - let pairs = record.decode_pairs(); - let public_key = pairs.secp256k1.ok_or(NodeError::MissingField( + let record = NodeRecord::from_enr_url(enr)?; + let public_key = record.pairs.secp256k1.ok_or(NodeError::MissingField( "public key not found in record".into(), ))?; let verifying_key = PublicKey::from_slice(public_key.as_bytes()).map_err(|_| { @@ -200,7 +198,7 @@ impl Node { let encoded = verifying_key.serialize_uncompressed(); let public_key = H512::from_slice(&encoded[1..]); - let ip: IpAddr = match (pairs.ip, pairs.ip6) { + let ip: IpAddr = match (record.pairs.ip, record.pairs.ip6) { (None, None) => { return Err(NodeError::MissingField( "Ip not found in record, can't construct node".into(), @@ -213,13 +211,15 @@ impl Node { // both udp and tcp can be defined in the pairs or only one // in the latter case, we have to default both ports to the one provided - let udp_port = pairs + let udp_port = record + .pairs .udp_port - .or(pairs.tcp_port) + .or(record.pairs.tcp_port) .ok_or(NodeError::MissingField("No port found in record".into()))?; - let tcp_port = pairs + let tcp_port = record + .pairs .tcp_port - .or(pairs.udp_port) + .or(record.pairs.udp_port) .ok_or(NodeError::MissingField("No port found in record".into()))?; Ok(Self::new(ip, udp_port, tcp_port, public_key)) @@ -264,17 +264,7 @@ impl Display for Node { } } -/// Reference: [ENR records](https://github.com/ethereum/devp2p/blob/master/enr.md) -#[derive(Debug, PartialEq, Clone, Eq, Default, Serialize, Deserialize)] -pub struct NodeRecord { - pub signature: H512, - pub seq: u64, - // holds optional values in (key, value) format - // value represents the rlp encoded bytes - pub pairs: Vec<(Bytes, Bytes)>, -} - -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct NodeRecordPairs { /// The ID of the identity scheme: https://github.com/ethereum/devp2p/blob/master/enr.md#v4-identity-scheme /// This is always "v4". @@ -287,15 +277,16 @@ pub struct NodeRecordPairs { pub tcp_port: Option, pub udp_port: Option, pub secp256k1: Option, + pub snap: Option>, // https://github.com/ethereum/devp2p/blob/master/enr-entries/eth.md - pub eth: Option, + pub eth: Option, // TODO implement ipv6 specific ports } -impl NodeRecord { - pub fn decode_pairs(&self) -> NodeRecordPairs { +impl NodeRecordPairs { + pub fn decode_pairs(pairs: Vec<(Bytes, Bytes)>) -> NodeRecordPairs { let mut decoded_pairs = NodeRecordPairs::default(); - for (key, value) in &self.pairs { + for (key, value) in pairs { let Ok(key) = String::from_utf8(key.to_vec()) else { continue; }; @@ -316,19 +307,17 @@ impl NodeRecord { decoded_pairs.secp256k1 = Some(H264::from_slice(&bytes)) } "eth" => { - // https://github.com/ethereum/devp2p/blob/master/enr-entries/eth.md - // entry-value = [[ forkHash, forkNext ], ...] + // TODO(#3494): here we decode as optional to ignore any errors, + // but we should return an error if we can't decode it + decoded_pairs.eth = EthEnrEntry::decode(&value).ok(); + } + "snap" => { let Ok(decoder) = Decoder::new(&value) else { continue; }; - // Here we decode fork-id = [ forkHash, forkNext ] - // TODO(#3494): here we decode as optional to ignore any errors, - // but we should return an error if we can't decode it - let (fork_id, decoder) = decoder.decode_optional_field(); - - // As per the spec, we should ignore any additional list elements in entry-value - decoder.finish_unchecked(); - decoded_pairs.eth = fork_id; + decoded_pairs.snap = decode_node_record_optional_fields(vec![], decoder) + .ok() + .map(|v| v.0); } _ => {} } @@ -337,6 +326,59 @@ impl NodeRecord { decoded_pairs } + /// Returns vector as (key,value) pairs encoded as rlp, and entries are chronologically ordered by keys. + pub fn encode_pairs(&self) -> Vec<(Bytes, Bytes)> { + let mut pairs = vec![]; + + if let Some(v) = self.eth.as_ref() { + pairs.push(("eth".into(), v.encode_to_vec().into())); + } + + if let Some(v) = self.id.as_ref() { + pairs.push(("id".into(), v.encode_to_vec().into())); + } + + if let Some(v) = self.ip.as_ref() { + pairs.push(("ip".into(), v.encode_to_vec().into())); + } + + if let Some(v) = self.ip6.as_ref() { + pairs.push(("ip6".into(), v.encode_to_vec().into())); + } + + if let Some(v) = self.secp256k1.as_ref() { + pairs.push(("secp256k1".into(), v.encode_to_vec().into())); + } + + if let Some(snap_pairs) = self.snap.as_ref() { + let mut snap_rlp = Vec::new(); + structs::Encoder::new(&mut snap_rlp) + .encode_key_value_list::(snap_pairs) + .finish(); + pairs.push(("snap".into(), snap_rlp.into())); + } + + if let Some(tcp_port) = self.tcp_port { + pairs.push(("tcp".into(), tcp_port.encode_to_vec().into())); + } + if let Some(udp_port) = self.udp_port { + pairs.push(("udp".into(), udp_port.encode_to_vec().into())); + } + pairs + } +} + +/// Reference: [ENR records](https://github.com/ethereum/devp2p/blob/master/enr.md) +#[derive(Debug, PartialEq, Clone, Eq, Default, Serialize, Deserialize)] +pub struct NodeRecord { + pub signature: H512, + pub seq: u64, + // holds optional values in (key, value) format + // value represents the rlp encoded bytes + pub pairs: NodeRecordPairs, +} + +impl NodeRecord { pub fn enr_url(&self) -> Result { let rlp_encoded = self.encode_to_vec(); let base64_encoded = ethrex_common::base64::encode(&rlp_encoded); @@ -347,30 +389,51 @@ impl NodeRecord { Ok(result) } - pub fn from_node(node: &Node, seq: u64, signer: &SecretKey) -> Result { + pub fn from_enr_url(enr: &str) -> Result { + let base64_decoded = ethrex_common::base64::decode(&enr.as_bytes()[4..]); + NodeRecord::decode(&base64_decoded).map_err(NodeError::from) + } + + pub fn from_node( + node: &Node, + seq: u64, + signer: &SecretKey, + fork_id: Option, + ) -> Result { + let mut record_pairs = NodeRecordPairs { + id: Some("v4".to_string()), + tcp_port: Some(node.tcp_port), + udp_port: Some(node.udp_port), + ..Default::default() + }; + + match node.ip { + IpAddr::V4(v4) => record_pairs.ip = Some(v4), + IpAddr::V6(v6) => record_pairs.ip6 = Some(v6), + } + + if let Some(fork_id) = fork_id { + let eth_enr = EthEnrEntry { + fork_id, + rest: vec![], + }; + record_pairs.eth = Some(eth_enr); + } + + record_pairs.secp256k1 = Some(H264::from_slice( + &PublicKey::from_secret_key(secp256k1::SECP256K1, signer).serialize(), + )); let mut record = NodeRecord { seq, + pairs: record_pairs, ..Default::default() }; - record - .pairs - .push(("id".into(), "v4".encode_to_vec().into())); - record - .pairs - .push(("ip".into(), node.ip.encode_to_vec().into())); - record.pairs.push(( - "secp256k1".into(), - PublicKey::from_secret_key(secp256k1::SECP256K1, signer) - .serialize() - .encode_to_vec() - .into(), - )); - record - .pairs - .push(("tcp".into(), node.tcp_port.encode_to_vec().into())); - record - .pairs - .push(("udp".into(), node.udp_port.encode_to_vec().into())); + + //TODO: Maybe we should sort the pairs based on key values? + //e.g. record.pairs.sort_by(|a, b| a.0.cmp(&b.0)); + //The keys are Bytes which implements Ord, so they can be compared directly. The sorting + //will be lexicographic (alphabetical for string keys like "eth", "id", "ip", etc.). + //Otherwise we get `record key/value pairs are not sorted by key` in hive tests. record.signature = record.sign_record(signer)?; @@ -392,7 +455,7 @@ impl NodeRecord { let mut rlp = vec![]; structs::Encoder::new(&mut rlp) .encode_field(&self.seq) - .encode_key_value_list::(&self.pairs) + .encode_key_value_list::(&self.pairs.encode_pairs()) .finish(); keccak_hash(&rlp) } @@ -400,29 +463,7 @@ impl NodeRecord { impl From for Vec<(Bytes, Bytes)> { fn from(value: NodeRecordPairs) -> Self { - let mut pairs = vec![]; - if let Some(eth) = value.eth { - pairs.push(("eth".into(), eth.encode_to_vec().into())); - } - if let Some(id) = value.id { - pairs.push(("id".into(), id.encode_to_vec().into())); - } - if let Some(ip) = value.ip { - pairs.push(("ip".into(), ip.encode_to_vec().into())); - } - if let Some(ip6) = value.ip6 { - pairs.push(("ip6".into(), ip6.encode_to_vec().into())); - } - if let Some(secp256k1) = value.secp256k1 { - pairs.push(("secp256k1".into(), secp256k1.encode_to_vec().into())); - } - if let Some(tcp) = value.tcp_port { - pairs.push(("tcp".into(), tcp.encode_to_vec().into())); - } - if let Some(udp) = value.udp_port { - pairs.push(("udp".into(), udp.encode_to_vec().into())); - } - pairs + value.encode_pairs() } } @@ -436,13 +477,14 @@ impl RLPDecode for NodeRecord { let (seq, decoder) = decoder.decode_field("seq")?; let (pairs, decoder) = decode_node_record_optional_fields(vec![], decoder)?; + let record_pairs = NodeRecordPairs::decode_pairs(pairs); + // all fields in pairs are optional except for id - let id_pair = pairs.iter().find(|(k, _v)| k.eq("id".as_bytes())); - if id_pair.is_some() { + if record_pairs.id.is_some() { let node_record = NodeRecord { signature, seq, - pairs, + pairs: record_pairs, }; let remaining = decoder.finish()?; Ok((node_record, remaining)) @@ -477,7 +519,7 @@ impl RLPEncode for NodeRecord { structs::Encoder::new(buf) .encode_field(&self.signature) .encode_field(&self.seq) - .encode_key_value_list::(&self.pairs) + .encode_key_value_list::(&self.pairs.encode_pairs()) .finish(); } } @@ -493,10 +535,39 @@ impl RLPEncode for Node { } } +// https://github.com/ethereum/devp2p/blob/master/enr-entries/eth.md +// entry-value = [[ forkHash, forkNext ], ...] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EthEnrEntry { + fork_id: ForkId, // Fork identifier per EIP-2124 + // Ignore additional fields (for forward compatibility). + rest: Vec<(Bytes, Bytes)>, +} + +impl RLPEncode for EthEnrEntry { + fn encode(&self, buf: &mut dyn BufMut) { + structs::Encoder::new(buf) + .encode_field(&self.fork_id) + .encode_key_value_list::(&self.rest) + .finish(); + } +} + +impl RLPDecode for EthEnrEntry { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + // Here we decode fork-id = [ forkHash, forkNext ] + let (fork_id, decoder) = decoder.decode_field("forkId")?; + let (rest, decoder) = decode_node_record_optional_fields(vec![], decoder)?; + let remaining = decoder.finish()?; + Ok((EthEnrEntry { fork_id, rest }, remaining)) + } +} + #[cfg(test)] mod tests { use crate::{ - types::{Node, NodeRecord}, + types::{EthEnrEntry, Node, NodeRecord}, utils::public_key_from_signing_key, }; use ethrex_common::H512; @@ -581,13 +652,48 @@ mod tests { addr.port(), public_key_from_signing_key(&signer), ); - let mut record = NodeRecord::from_node(&node, 1, &signer).unwrap(); // Drop fork ID since the test doesn't use it - record.pairs.retain(|(k, _)| k != "eth"); + let record = NodeRecord::from_node(&node, 1, &signer, None).unwrap(); record.sign_record(&signer).unwrap(); let expected_enr_string = "enr:-Iu4QIQVZPoFHwH3TCVkFKpW3hm28yj5HteKEO0QTVsavAGgD9ISdBmAgsIyUzdD9Yrqc84EhT067h1VA1E1HSLKcMgBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQJtSDUljLLg3EYuRCp8QJvH8G2F9rmUAQtPKlZjq_O7loN0Y3CCdl-DdWRwgnZf"; assert_eq!(record.enr_url().unwrap(), expected_enr_string); } + + #[tokio::test] + async fn encode_decode_node_record_with_forkid() { + let signer = SecretKey::from_slice(&[ + 16, 125, 177, 238, 167, 212, 168, 215, 239, 165, 77, 224, 199, 143, 55, 205, 9, 194, + 87, 139, 92, 46, 30, 191, 74, 37, 68, 242, 38, 225, 104, 246, + ]) + .unwrap(); + let addr = std::net::SocketAddr::from_str("127.0.0.1:30303").unwrap(); + + let mut storage = + Store::new("", EngineType::InMemory).expect("Failed to create in-memory storage"); + storage + .add_initial_state(serde_json::from_str(TEST_GENESIS).unwrap()) + .await + .expect("Failed to build test genesis"); + + let node = Node::new( + addr.ip(), + addr.port(), + addr.port(), + public_key_from_signing_key(&signer), + ); + let fork_id = storage.get_fork_id().await.unwrap(); + let eth_enr = EthEnrEntry { + fork_id: fork_id.clone(), + rest: vec![], + }; + + let record = NodeRecord::from_node(&node, 1, &signer, Some(fork_id)).unwrap(); + + let enr_url = record.enr_url().unwrap(); + let parsed_record = NodeRecord::from_enr_url(&enr_url).unwrap(); + + assert_eq!(parsed_record.pairs.eth, Some(eth_enr)); + } } diff --git a/crates/networking/rpc/admin/peers.rs b/crates/networking/rpc/admin/peers.rs index 95525cfcf2c..e626c5d9d2b 100644 --- a/crates/networking/rpc/admin/peers.rs +++ b/crates/networking/rpc/admin/peers.rs @@ -167,7 +167,7 @@ mod tests { fn test_peer_data_to_serialized_peer() { // Test that we can correctly serialize an active Peer let node = Node::from_enode_url("enode://4aeb4ab6c14b23e2c4cfdce879c04b0748a20d8e9b59e25ded2a08143e265c6c25936e74cbc8e641e3312ca288673d91f2f93f8e277de3cfa444ecdaaf982052@157.90.35.166:30303").unwrap(); - let record = NodeRecord::from_node(&node, 17, &SecretKey::new(&mut OsRng)).unwrap(); + let record = NodeRecord::from_node(&node, 17, &SecretKey::new(&mut OsRng), None).unwrap(); let mut peer = PeerData::new( node, Some(record), diff --git a/crates/networking/rpc/test_utils.rs b/crates/networking/rpc/test_utils.rs index 1b29f45e4eb..540ca2e184a 100644 --- a/crates/networking/rpc/test_utils.rs +++ b/crates/networking/rpc/test_utils.rs @@ -208,7 +208,7 @@ pub fn example_local_node_record() -> NodeRecord { let node = Node::new("127.0.0.1".parse().unwrap(), 30303, 30303, public_key_1); let signer = SecretKey::new(&mut rand::rngs::OsRng); - NodeRecord::from_node(&node, 1, &signer).unwrap() + NodeRecord::from_node(&node, 1, &signer, None).unwrap() } // Util to start an api for testing on ports 8500 and 8501,