From 6313699e4090790ade934f85fd99d3fa96497051 Mon Sep 17 00:00:00 2001 From: lakshya-sky Date: Mon, 17 Nov 2025 14:26:15 -0500 Subject: [PATCH 1/4] feat(l1): add "eth" entry to local ENR --- cmd/ethrex/initializers.rs | 4 +- crates/networking/p2p/discv4/server.rs | 5 ++- crates/networking/p2p/network.rs | 1 + crates/networking/p2p/types.rs | 58 +++++++++++++++++++++++++- crates/networking/rpc/admin/peers.rs | 2 +- crates/networking/rpc/test_utils.rs | 2 +- 6 files changed, 65 insertions(+), 7 deletions(-) diff --git a/cmd/ethrex/initializers.rs b/cmd/ethrex/initializers.rs index 163071df6d1..dd92894edb8 100644 --- a/cmd/ethrex/initializers.rs +++ b/cmd/ethrex/initializers.rs @@ -333,7 +333,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") } _ => { @@ -341,7 +341,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/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..66bf705ef03 100644 --- a/crates/networking/p2p/types.rs +++ b/crates/networking/p2p/types.rs @@ -347,11 +347,23 @@ impl NodeRecord { Ok(result) } - pub fn from_node(node: &Node, seq: u64, signer: &SecretKey) -> Result { + pub fn from_node( + node: &Node, + seq: u64, + signer: &SecretKey, + fork_id: Option, + ) -> Result { let mut record = NodeRecord { seq, ..Default::default() }; + if let Some(fork_id) = fork_id { + // entry-value = [[ forkHash, forkNext ], ...] + let eth: (ForkId, ()) = (fork_id, ()); + record + .pairs + .push(("eth".into(), eth.encode_to_vec().into())); + } record .pairs .push(("id".into(), "v4".encode_to_vec().into())); @@ -372,6 +384,12 @@ impl NodeRecord { .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)?; Ok(record) @@ -500,6 +518,7 @@ mod tests { utils::public_key_from_signing_key, }; use ethrex_common::H512; + use ethrex_rlp::decode::RLPDecode; use ethrex_storage::{EngineType, Store}; use secp256k1::SecretKey; use std::{net::SocketAddr, str::FromStr}; @@ -581,7 +600,7 @@ mod tests { addr.port(), public_key_from_signing_key(&signer), ); - let mut record = NodeRecord::from_node(&node, 1, &signer).unwrap(); + let mut record = NodeRecord::from_node(&node, 1, &signer, None).unwrap(); // Drop fork ID since the test doesn't use it record.pairs.retain(|(k, _)| k != "eth"); record.sign_record(&signer).unwrap(); @@ -590,4 +609,39 @@ mod tests { 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 record = NodeRecord::from_node(&node, 1, &signer, Some(fork_id.clone())).unwrap(); + record.sign_record(&signer).unwrap(); + + let enr_url = record.enr_url().unwrap(); + let base64_decoded = ethrex_common::base64::decode(&enr_url.as_bytes()[4..]); + let parsed_record = NodeRecord::decode(&base64_decoded).unwrap(); + let pairs = parsed_record.decode_pairs(); + + assert_eq!(pairs.eth, Some(fork_id)); + } } 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, From 4e7bc89cdf9c2a6cd9c70bbf39f598e09ab98e50 Mon Sep 17 00:00:00 2001 From: lakshya-sky Date: Fri, 28 Nov 2025 09:50:36 -0500 Subject: [PATCH 2/4] use correct type and improve comments --- crates/networking/p2p/types.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/networking/p2p/types.rs b/crates/networking/p2p/types.rs index 66bf705ef03..31d83a4462b 100644 --- a/crates/networking/p2p/types.rs +++ b/crates/networking/p2p/types.rs @@ -358,8 +358,11 @@ impl NodeRecord { ..Default::default() }; if let Some(fork_id) = fork_id { - // entry-value = [[ forkHash, forkNext ], ...] - let eth: (ForkId, ()) = (fork_id, ()); + // Without the Vec wrapper, RLP encoding fork_id directly would produce: + // [forkHash, forkNext] + // But the spec requires nested lists: + // [[forkHash, forkNext]] + let eth = vec![fork_id]; record .pairs .push(("eth".into(), eth.encode_to_vec().into())); @@ -384,12 +387,6 @@ impl NodeRecord { .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)?; Ok(record) @@ -420,6 +417,11 @@ impl From for Vec<(Bytes, Bytes)> { fn from(value: NodeRecordPairs) -> Self { let mut pairs = vec![]; if let Some(eth) = value.eth { + // Without the Vec wrapper, RLP encoding fork_id directly would produce: + // [forkHash, forkNext] + // But the spec requires nested lists: + // [[forkHash, forkNext]] + let eth = vec![eth]; pairs.push(("eth".into(), eth.encode_to_vec().into())); } if let Some(id) = value.id { From ef461ae96d4721af845ffc4cec9215156f611bec Mon Sep 17 00:00:00 2001 From: lakshya-sky Date: Fri, 28 Nov 2025 14:48:01 -0500 Subject: [PATCH 3/4] use helper method for setting fork-id --- cmd/ethrex/initializers.rs | 4 +-- crates/networking/p2p/discv4/server.rs | 7 +++-- crates/networking/p2p/types.rs | 38 ++++++++++++++------------ crates/networking/rpc/admin/peers.rs | 2 +- crates/networking/rpc/test_utils.rs | 2 +- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/cmd/ethrex/initializers.rs b/cmd/ethrex/initializers.rs index dd92894edb8..163071df6d1 100644 --- a/cmd/ethrex/initializers.rs +++ b/cmd/ethrex/initializers.rs @@ -333,7 +333,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, None) + NodeRecord::from_node(local_p2p_node, config.node_record.seq + 1, signer) .expect("Node record could not be created from local node") } _ => { @@ -341,7 +341,7 @@ pub fn get_local_node_record( .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); - NodeRecord::from_node(local_p2p_node, timestamp, signer, None) + NodeRecord::from_node(local_p2p_node, timestamp, signer) .expect("Node record could not be created from local node") } } diff --git a/crates/networking/p2p/discv4/server.rs b/crates/networking/p2p/discv4/server.rs index 21f6ae433c4..fe34ec1ed31 100644 --- a/crates/networking/p2p/discv4/server.rs +++ b/crates/networking/p2p/discv4/server.rs @@ -99,9 +99,12 @@ impl DiscoveryServer { ) -> Result<(), DiscoveryServerError> { info!("Starting Discovery Server"); - let fork_id = storage.get_fork_id().await.ok(); - let local_node_record = NodeRecord::from_node(&local_node, 1, &signer, fork_id) + let mut local_node_record = NodeRecord::from_node(&local_node, 1, &signer) .expect("Failed to create local node record"); + if let Ok(fork_id) = storage.get_fork_id().await { + local_node_record.set_fork_id(fork_id); + } + let mut discovery_server = Self { local_node: local_node.clone(), local_node_record, diff --git a/crates/networking/p2p/types.rs b/crates/networking/p2p/types.rs index 31d83a4462b..9d194b8e6ec 100644 --- a/crates/networking/p2p/types.rs +++ b/crates/networking/p2p/types.rs @@ -271,6 +271,7 @@ pub struct NodeRecord { pub seq: u64, // holds optional values in (key, value) format // value represents the rlp encoded bytes + // The key/value pairs must be sorted by key and must be unique pub pairs: Vec<(Bytes, Bytes)>, } @@ -347,26 +348,11 @@ impl NodeRecord { Ok(result) } - pub fn from_node( - node: &Node, - seq: u64, - signer: &SecretKey, - fork_id: Option, - ) -> Result { + pub fn from_node(node: &Node, seq: u64, signer: &SecretKey) -> Result { let mut record = NodeRecord { seq, ..Default::default() }; - if let Some(fork_id) = fork_id { - // Without the Vec wrapper, RLP encoding fork_id directly would produce: - // [forkHash, forkNext] - // But the spec requires nested lists: - // [[forkHash, forkNext]] - let eth = vec![fork_id]; - record - .pairs - .push(("eth".into(), eth.encode_to_vec().into())); - } record .pairs .push(("id".into(), "v4".encode_to_vec().into())); @@ -392,6 +378,20 @@ impl NodeRecord { Ok(record) } + pub fn set_fork_id(&mut self, fork_id: ForkId) { + // Without the Vec wrapper, RLP encoding fork_id directly would produce: + // [forkHash, forkNext] + // But the spec requires nested lists: + // [[forkHash, forkNext]] + let eth = vec![fork_id]; + self.pairs.push(("eth".into(), eth.encode_to_vec().into())); + + //Pairs need to be sorted by their key. + //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.). + self.pairs.sort_by(|a, b| a.0.cmp(&b.0)); + } + fn sign_record(&self, signer: &SecretKey) -> Result { let digest = &self.get_signature_digest(); let msg = secp256k1::Message::from_digest_slice(digest) @@ -602,7 +602,7 @@ mod tests { addr.port(), public_key_from_signing_key(&signer), ); - let mut record = NodeRecord::from_node(&node, 1, &signer, None).unwrap(); + 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"); record.sign_record(&signer).unwrap(); @@ -636,7 +636,9 @@ mod tests { ); let fork_id = storage.get_fork_id().await.unwrap(); - let record = NodeRecord::from_node(&node, 1, &signer, Some(fork_id.clone())).unwrap(); + let mut record = NodeRecord::from_node(&node, 1, &signer).unwrap(); + record.set_fork_id(fork_id.clone()); + record.sign_record(&signer).unwrap(); let enr_url = record.enr_url().unwrap(); diff --git a/crates/networking/rpc/admin/peers.rs b/crates/networking/rpc/admin/peers.rs index e626c5d9d2b..95525cfcf2c 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), None).unwrap(); + let record = NodeRecord::from_node(&node, 17, &SecretKey::new(&mut OsRng)).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 540ca2e184a..1b29f45e4eb 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, None).unwrap() + NodeRecord::from_node(&node, 1, &signer).unwrap() } // Util to start an api for testing on ports 8500 and 8501, From 4459010afedfe3aa3dc07196c33a9f24dc529862 Mon Sep 17 00:00:00 2001 From: lakshya-sky Date: Fri, 28 Nov 2025 15:16:04 -0500 Subject: [PATCH 4/4] fix signature after seting fork-id --- crates/networking/p2p/discv4/server.rs | 4 +++- crates/networking/p2p/types.rs | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/networking/p2p/discv4/server.rs b/crates/networking/p2p/discv4/server.rs index fe34ec1ed31..88becf7beba 100644 --- a/crates/networking/p2p/discv4/server.rs +++ b/crates/networking/p2p/discv4/server.rs @@ -102,7 +102,9 @@ impl DiscoveryServer { let mut local_node_record = NodeRecord::from_node(&local_node, 1, &signer) .expect("Failed to create local node record"); if let Ok(fork_id) = storage.get_fork_id().await { - local_node_record.set_fork_id(fork_id); + local_node_record + .set_fork_id(fork_id, &signer) + .expect("Failed to set fork_id on local node record"); } let mut discovery_server = Self { diff --git a/crates/networking/p2p/types.rs b/crates/networking/p2p/types.rs index 9d194b8e6ec..2073db1d7d5 100644 --- a/crates/networking/p2p/types.rs +++ b/crates/networking/p2p/types.rs @@ -378,7 +378,7 @@ impl NodeRecord { Ok(record) } - pub fn set_fork_id(&mut self, fork_id: ForkId) { + pub fn set_fork_id(&mut self, fork_id: ForkId, signer: &SecretKey) -> Result<(), NodeError> { // Without the Vec wrapper, RLP encoding fork_id directly would produce: // [forkHash, forkNext] // But the spec requires nested lists: @@ -390,6 +390,9 @@ impl NodeRecord { //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.). self.pairs.sort_by(|a, b| a.0.cmp(&b.0)); + + self.signature = self.sign_record(signer)?; + Ok(()) } fn sign_record(&self, signer: &SecretKey) -> Result { @@ -637,7 +640,7 @@ mod tests { let fork_id = storage.get_fork_id().await.unwrap(); let mut record = NodeRecord::from_node(&node, 1, &signer).unwrap(); - record.set_fork_id(fork_id.clone()); + record.set_fork_id(fork_id.clone(), &signer).unwrap(); record.sign_record(&signer).unwrap();