Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions crates/config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ mod tests {
use crate::PruneConfig;
use alloy_primitives::Address;
use reth_network_peers::TrustedPeer;
use reth_network_types::peers::Discv5BootNode;
use reth_prune_types::{PruneMode, PruneModes, ReceiptsLogPruneConfig};
use std::{collections::BTreeMap, path::Path, str::FromStr, time::Duration};

Expand Down Expand Up @@ -1164,4 +1165,119 @@ connect_trusted_nodes_only = true
assert!(conf.peers.trusted_nodes.contains(&node));
}
}

#[test]
fn test_bootnodes_v4_config() {
let reth_toml = r#"
[peers]
bootnodes_v4 = [
"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303",
"enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303"
]
"#;

let conf: Config = toml::from_str(reth_toml).unwrap();
assert_eq!(conf.peers.bootnodes_v4.len(), 2);

let expected_enodes = vec![
"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303",
"enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303",
];

for enode in expected_enodes {
let node = TrustedPeer::from_str(enode).unwrap();
assert!(conf.peers.bootnodes_v4.contains(&node));
}
}

#[test]
fn test_bootnodes_v5_config_enode_rejected() {
let reth_toml = r#"
[peers]
bootnodes_v5 = [
"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303"
]
"#;

// Enode strings should be rejected when parsing Discv5BootNode
let result: Result<Config, _> = toml::from_str(reth_toml);
assert!(result.is_err(), "Enode strings should be rejected for Discv5 bootnodes");
}

#[test]
fn test_bootnodes_v5_config_enr() {
let reth_toml = r#"
[peers]
bootnodes_v5 = [
"enr:-J64QBwRIWAco7lv6jImSOjPU_W266lHXzpAS5YOh7WmgTyBZkgLgOwo_mxKJq3wz2XRbsoBItbv1dCyjIoNq67mFguGAYrTxM42gmlkgnY0gmlwhBLSsHKHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDmoWSi8hcsRpQf2eJsNUx-sqv6fH4btmo2HsAzZFAKnKDdGNwgiQGg3VkcIIkBg"
]
"#;

let conf: Config = toml::from_str(reth_toml).unwrap();
assert_eq!(conf.peers.bootnodes_v5.len(), 1);

let expected_enr = "enr:-J64QBwRIWAco7lv6jImSOjPU_W266lHXzpAS5YOh7WmgTyBZkgLgOwo_mxKJq3wz2XRbsoBItbv1dCyjIoNq67mFguGAYrTxM42gmlkgnY0gmlwhBLSsHKHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDmoWSi8hcsRpQf2eJsNUx-sqv6fH4btmo2HsAzZFAKnKDdGNwgiQGg3VkcIIkBg";
let bootnode = Discv5BootNode::from_str(expected_enr).unwrap();
assert!(conf.peers.bootnodes_v5.contains(&bootnode));
// Verify the ENR was correctly parsed
let stored_enr = &conf.peers.bootnodes_v5[0];
assert_eq!(stored_enr.as_str(), expected_enr);
}

#[test]
fn test_bootnodes_v5_config_multiple_enr() {
let reth_toml = r#"
[peers]
bootnodes_v5 = [
"enr:-J64QBwRIWAco7lv6jImSOjPU_W266lHXzpAS5YOh7WmgTyBZkgLgOwo_mxKJq3wz2XRbsoBItbv1dCyjIoNq67mFguGAYrTxM42gmlkgnY0gmlwhBLSsHKHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDmoWSi8hcsRpQf2eJsNUx-sqv6fH4btmo2HsAzZFAKnKDdGNwgiQGg3VkcIIkBg",
"enr:-J64QFa3qMsONLGphfjEkeYyF6Jkil_jCuJmm7_a42ckZeUQGLVzrzstZNb1dgBp1GGx9bzImq5VxJLP-BaptZThGiWGAYrTytOvgmlkgnY0gmlwhGsV-zeHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDahfSECTIS_cXyZ8IyNf4leANlZnrsMEWTkEYxf4GMCmDdGNwgiQGg3VkcIIkBg"
]
"#;

let conf: Config = toml::from_str(reth_toml).unwrap();
assert_eq!(conf.peers.bootnodes_v5.len(), 2);

let enr1 = "enr:-J64QBwRIWAco7lv6jImSOjPU_W266lHXzpAS5YOh7WmgTyBZkgLgOwo_mxKJq3wz2XRbsoBItbv1dCyjIoNq67mFguGAYrTxM42gmlkgnY0gmlwhBLSsHKHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDmoWSi8hcsRpQf2eJsNUx-sqv6fH4btmo2HsAzZFAKnKDdGNwgiQGg3VkcIIkBg";
let enr2 = "enr:-J64QFa3qMsONLGphfjEkeYyF6Jkil_jCuJmm7_a42ckZeUQGLVzrzstZNb1dgBp1GGx9bzImq5VxJLP-BaptZThGiWGAYrTytOvgmlkgnY0gmlwhGsV-zeHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDahfSECTIS_cXyZ8IyNf4leANlZnrsMEWTkEYxf4GMCmDdGNwgiQGg3VkcIIkBg";

let bootnode1 = Discv5BootNode::from_str(enr1).unwrap();
let bootnode2 = Discv5BootNode::from_str(enr2).unwrap();
assert!(conf.peers.bootnodes_v5.contains(&bootnode1));
assert!(conf.peers.bootnodes_v5.contains(&bootnode2));
}

#[test]
fn test_bootnodes_v4_and_v5_separate_config() {
let reth_toml = r#"
[peers]
bootnodes_v4 = [
"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303"
]
bootnodes_v5 = [
"enr:-J64QBwRIWAco7lv6jImSOjPU_W266lHXzpAS5YOh7WmgTyBZkgLgOwo_mxKJq3wz2XRbsoBItbv1dCyjIoNq67mFguGAYrTxM42gmlkgnY0gmlwhBLSsHKHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDmoWSi8hcsRpQf2eJsNUx-sqv6fH4btmo2HsAzZFAKnKDdGNwgiQGg3VkcIIkBg"
]
"#;

let conf: Config = toml::from_str(reth_toml).unwrap();
assert_eq!(conf.peers.bootnodes_v4.len(), 1);
assert_eq!(conf.peers.bootnodes_v5.len(), 1);

let v4_node = TrustedPeer::from_str(
"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303",
)
.unwrap();
let v5_enr = "enr:-J64QBwRIWAco7lv6jImSOjPU_W266lHXzpAS5YOh7WmgTyBZkgLgOwo_mxKJq3wz2XRbsoBItbv1dCyjIoNq67mFguGAYrTxM42gmlkgnY0gmlwhBLSsHKHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDmoWSi8hcsRpQf2eJsNUx-sqv6fH4btmo2HsAzZFAKnKDdGNwgiQGg3VkcIIkBg";
let v5_bootnode = Discv5BootNode::from_str(v5_enr).unwrap();

assert!(conf.peers.bootnodes_v4.contains(&v4_node));
assert!(conf.peers.bootnodes_v5.contains(&v5_bootnode));
assert_eq!(conf.peers.bootnodes_v5[0].as_str(), v5_enr);
}

#[test]
fn test_bootnodes_default_empty() {
let conf: Config = Config::default();
assert!(conf.peers.bootnodes_v4.is_empty());
assert!(conf.peers.bootnodes_v5.is_empty());
}
}
85 changes: 85 additions & 0 deletions crates/net/network-types/src/peers/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::{
collections::HashSet,
io::{self, ErrorKind},
path::Path,
str::FromStr,
time::Duration,
};

Expand All @@ -13,6 +14,64 @@ use tracing::info;

use crate::{BackoffKind, ReputationChangeWeights};

/// A bootnode for Discv5 discovery (ENR only).
///
/// This is a newtype wrapper around a string to provide a clearer API.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Discv5BootNode(pub String);

impl Discv5BootNode {
/// Returns a reference to the underlying ENR string.
pub fn as_str(&self) -> &str {
&self.0
}

/// Returns the ENR as a base64-encoded string.
pub fn to_base64(&self) -> String {
// Remove "enr:" prefix if present
self.0.strip_prefix("enr:").unwrap_or(&self.0).to_string()
}
}

impl FromStr for Discv5BootNode {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// Basic validation: should start with "enr:" and be non-empty
if !s.starts_with("enr:") || s.len() <= 4 {
return Err("Invalid ENR string: must start with 'enr:'".to_string());
}
Ok(Self(s.to_string()))
}
}

#[cfg(feature = "serde")]
mod serde_impl {
use super::Discv5BootNode;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

impl Serialize for Discv5BootNode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Serialize as ENR string (ensure "enr:" prefix)
let s = if self.0.starts_with("enr:") { &self.0 } else { &format!("enr:{}", self.0) };
serializer.serialize_str(s)
}
}

impl<'de> Deserialize<'de> for Discv5BootNode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
}

/// Maximum number of available slots for outbound sessions.
pub const DEFAULT_MAX_COUNT_PEERS_OUTBOUND: u32 = 100;

Expand Down Expand Up @@ -172,6 +231,18 @@ pub struct PeersConfig {
/// IPs within the specified CIDR ranges will be allowed.
#[cfg_attr(feature = "serde", serde(skip))]
pub ip_filter: IpFilter,
/// Bootnodes for Discv4 discovery (enode:// format).
///
/// Similar to geth's `BootstrapNodes`. These nodes are used to bootstrap the Discv4
/// discovery protocol.
#[cfg_attr(feature = "serde", serde(default))]
pub bootnodes_v4: Vec<TrustedPeer>,
/// Bootnodes for Discv5 discovery (ENR format only).
///
/// Similar to geth's `BootstrapNodesV5`. These nodes are used to bootstrap the Discv5
/// discovery protocol. Must be ENR strings (enr:...).
#[cfg_attr(feature = "serde", serde(default))]
pub bootnodes_v5: Vec<Discv5BootNode>,
}

impl Default for PeersConfig {
Expand All @@ -191,6 +262,8 @@ impl Default for PeersConfig {
max_backoff_count: 5,
incoming_ip_throttle_duration: INBOUND_IP_THROTTLE_DURATION,
ip_filter: IpFilter::default(),
bootnodes_v4: Default::default(),
bootnodes_v5: Default::default(),
}
}
}
Expand Down Expand Up @@ -314,6 +387,18 @@ impl PeersConfig {
self
}

/// Sets the bootnodes for Discv4 discovery.
pub fn with_bootnodes_v4(mut self, bootnodes: Vec<TrustedPeer>) -> Self {
self.bootnodes_v4 = bootnodes;
self
}

/// Sets the bootnodes for Discv5 discovery.
pub fn with_bootnodes_v5(mut self, bootnodes: Vec<Discv5BootNode>) -> Self {
self.bootnodes_v5 = bootnodes;
self
}

/// Returns settings for testing
#[cfg(any(test, feature = "test-utils"))]
pub fn test() -> Self {
Expand Down
2 changes: 1 addition & 1 deletion crates/net/network-types/src/peers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub mod kind;
pub mod reputation;
pub mod state;

pub use config::{ConnectionsConfig, PeersConfig};
pub use config::{ConnectionsConfig, Discv5BootNode, PeersConfig};
pub use reputation::{Reputation, ReputationChange, ReputationChangeKind, ReputationChangeWeights};

use alloy_eip2124::ForkId;
Expand Down
2 changes: 2 additions & 0 deletions crates/net/network/src/peers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ impl PeersManager {
max_backoff_count,
incoming_ip_throttle_duration,
ip_filter,
bootnodes_v4: _,
bootnodes_v5: _,
} = config;
let (manager_tx, handle_rx) = mpsc::unbounded_channel();
let now = Instant::now();
Expand Down
Loading
Loading