1use std::{
4 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
5 ops::Not,
6 path::PathBuf,
7};
8
9use clap::Args;
10use reth_chainspec::EthChainSpec;
11use reth_config::Config;
12use reth_discv4::{NodeRecord, DEFAULT_DISCOVERY_ADDR, DEFAULT_DISCOVERY_PORT};
13use reth_discv5::{
14 discv5::ListenConfig, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_PORT,
15 DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL,
16};
17use reth_net_nat::{NatResolver, DEFAULT_NET_IF_NAME};
18use reth_network::{
19 transactions::{
20 config::TransactionPropagationKind,
21 constants::{
22 tx_fetcher::{
23 DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
24 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
25 },
26 tx_manager::{
27 DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
28 },
29 },
30 TransactionFetcherConfig, TransactionsManagerConfig,
31 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
32 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
33 },
34 HelloMessageWithProtocols, NetworkConfigBuilder, NetworkPrimitives, SessionsConfig,
35};
36use reth_network_peers::{mainnet_nodes, TrustedPeer};
37use secp256k1::SecretKey;
38use tracing::error;
39
40use crate::version::P2P_CLIENT_VERSION;
41
42#[derive(Debug, Clone, Args, PartialEq, Eq)]
44#[command(next_help_heading = "Networking")]
45pub struct NetworkArgs {
46 #[command(flatten)]
48 pub discovery: DiscoveryArgs,
49
50 #[expect(clippy::doc_markdown)]
51 #[arg(long, value_delimiter = ',')]
55 pub trusted_peers: Vec<TrustedPeer>,
56
57 #[arg(long)]
59 pub trusted_only: bool,
60
61 #[arg(long, value_delimiter = ',')]
65 pub bootnodes: Option<Vec<TrustedPeer>>,
66
67 #[arg(long, default_value_t = 0)]
69 pub dns_retries: usize,
70
71 #[arg(long, value_name = "FILE", verbatim_doc_comment, conflicts_with = "no_persist_peers")]
74 pub peers_file: Option<PathBuf>,
75
76 #[arg(long, value_name = "IDENTITY", default_value = P2P_CLIENT_VERSION)]
78 pub identity: String,
79
80 #[arg(long, value_name = "PATH")]
85 pub p2p_secret_key: Option<PathBuf>,
86
87 #[arg(long, verbatim_doc_comment)]
89 pub no_persist_peers: bool,
90
91 #[arg(long, default_value = "any")]
93 pub nat: NatResolver,
94
95 #[arg(long = "addr", value_name = "ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
97 pub addr: IpAddr,
98
99 #[arg(long = "port", value_name = "PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
101 pub port: u16,
102
103 #[arg(long)]
105 pub max_outbound_peers: Option<usize>,
106
107 #[arg(long)]
109 pub max_inbound_peers: Option<usize>,
110
111 #[arg(long = "max-tx-reqs", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS, verbatim_doc_comment)]
113 pub max_concurrent_tx_requests: u32,
114
115 #[arg(long = "max-tx-reqs-peer", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER, verbatim_doc_comment)]
117 pub max_concurrent_tx_requests_per_peer: u8,
118
119 #[arg(long = "max-seen-tx-history", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER, verbatim_doc_comment)]
123 pub max_seen_tx_history: u32,
124
125 #[arg(long = "max-pending-imports", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, verbatim_doc_comment)]
126 pub max_pending_pool_imports: usize,
128
129 #[arg(long = "pooled-tx-response-soft-limit", value_name = "BYTES", default_value_t = SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE, verbatim_doc_comment)]
133 pub soft_limit_byte_size_pooled_transactions_response: usize,
134
135 #[arg(long = "pooled-tx-pack-soft-limit", value_name = "BYTES", default_value_t = DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ, verbatim_doc_comment)]
147 pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
148
149 #[arg(long = "max-tx-pending-fetch", value_name = "COUNT", default_value_t = DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, verbatim_doc_comment)]
151 pub max_capacity_cache_txns_pending_fetch: u32,
152
153 #[arg(long = "net-if.experimental", conflicts_with = "addr", value_name = "IF_NAME")]
157 pub net_if: Option<String>,
158
159 #[arg(long = "tx-propagation-policy", default_value_t = TransactionPropagationKind::All)]
163 pub tx_propagation_policy: TransactionPropagationKind,
164}
165
166impl NetworkArgs {
167 pub fn resolved_addr(&self) -> IpAddr {
169 if let Some(ref if_name) = self.net_if {
170 let if_name = if if_name.is_empty() { DEFAULT_NET_IF_NAME } else { if_name };
171 return match reth_net_nat::net_if::resolve_net_if_ip(if_name) {
172 Ok(addr) => addr,
173 Err(err) => {
174 error!(target: "reth::cli",
175 if_name,
176 %err,
177 "Failed to read network interface IP"
178 );
179
180 DEFAULT_DISCOVERY_ADDR
181 }
182 };
183 }
184
185 self.addr
186 }
187
188 pub fn resolved_bootnodes(&self) -> Option<Vec<NodeRecord>> {
190 self.bootnodes.clone().map(|bootnodes| {
191 bootnodes.into_iter().filter_map(|node| node.resolve_blocking().ok()).collect()
192 })
193 }
194 pub fn transactions_manager_config(&self) -> TransactionsManagerConfig {
196 TransactionsManagerConfig {
197 transaction_fetcher_config: TransactionFetcherConfig::new(
198 self.max_concurrent_tx_requests,
199 self.max_concurrent_tx_requests_per_peer,
200 self.soft_limit_byte_size_pooled_transactions_response,
201 self.soft_limit_byte_size_pooled_transactions_response_on_pack_request,
202 self.max_capacity_cache_txns_pending_fetch,
203 ),
204 max_transactions_seen_by_peer_history: self.max_seen_tx_history,
205 propagation_mode: Default::default(),
206 }
207 }
208
209 pub fn network_config<N: NetworkPrimitives>(
221 &self,
222 config: &Config,
223 chain_spec: impl EthChainSpec,
224 secret_key: SecretKey,
225 default_peers_file: PathBuf,
226 ) -> NetworkConfigBuilder<N> {
227 let addr = self.resolved_addr();
228 let chain_bootnodes = self
229 .resolved_bootnodes()
230 .unwrap_or_else(|| chain_spec.bootnodes().unwrap_or_else(mainnet_nodes));
231 let peers_file = self.peers_file.clone().unwrap_or(default_peers_file);
232
233 let peers_config = config
235 .peers
236 .clone()
237 .with_max_inbound_opt(self.max_inbound_peers)
238 .with_max_outbound_opt(self.max_outbound_peers);
239
240 NetworkConfigBuilder::<N>::new(secret_key)
242 .peer_config(config.peers_config_with_basic_nodes_from_file(
243 self.persistent_peers_file(peers_file).as_deref(),
244 ))
245 .external_ip_resolver(self.nat)
246 .sessions_config(
247 SessionsConfig::default().with_upscaled_event_buffer(peers_config.max_peers()),
248 )
249 .peer_config(peers_config)
250 .boot_nodes(chain_bootnodes.clone())
251 .transactions_manager_config(self.transactions_manager_config())
252 .apply(|builder| {
254 let peer_id = builder.get_peer_id();
255 builder.hello_message(
256 HelloMessageWithProtocols::builder(peer_id)
257 .client_version(&self.identity)
258 .build(),
259 )
260 })
261 .apply(|builder| {
263 let rlpx_socket = (addr, self.port).into();
264 self.discovery.apply_to_builder(builder, rlpx_socket, chain_bootnodes)
265 })
266 .listener_addr(SocketAddr::new(
267 addr, self.port,
269 ))
270 .discovery_addr(SocketAddr::new(
271 self.discovery.addr,
272 self.discovery.port,
274 ))
275 }
276
277 pub fn persistent_peers_file(&self, peers_file: PathBuf) -> Option<PathBuf> {
279 self.no_persist_peers.not().then_some(peers_file)
280 }
281
282 pub const fn with_unused_p2p_port(mut self) -> Self {
285 self.port = 0;
286 self
287 }
288
289 pub const fn with_unused_ports(mut self) -> Self {
292 self = self.with_unused_p2p_port();
293 self.discovery = self.discovery.with_unused_discovery_port();
294 self
295 }
296
297 pub fn adjust_instance_ports(&mut self, instance: Option<u16>) {
303 if let Some(instance) = instance {
304 debug_assert_ne!(instance, 0, "instance must be non-zero");
305 self.port += instance - 1;
306 self.discovery.adjust_instance_ports(instance);
307 }
308 }
309
310 pub async fn resolve_trusted_peers(&self) -> Result<Vec<NodeRecord>, std::io::Error> {
312 futures::future::try_join_all(
313 self.trusted_peers.iter().map(|peer| async move { peer.resolve().await }),
314 )
315 .await
316 }
317}
318
319impl Default for NetworkArgs {
320 fn default() -> Self {
321 Self {
322 discovery: DiscoveryArgs::default(),
323 trusted_peers: vec![],
324 trusted_only: false,
325 bootnodes: None,
326 dns_retries: 0,
327 peers_file: None,
328 identity: P2P_CLIENT_VERSION.to_string(),
329 p2p_secret_key: None,
330 no_persist_peers: false,
331 nat: NatResolver::Any,
332 addr: DEFAULT_DISCOVERY_ADDR,
333 port: DEFAULT_DISCOVERY_PORT,
334 max_outbound_peers: None,
335 max_inbound_peers: None,
336 max_concurrent_tx_requests: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
337 max_concurrent_tx_requests_per_peer: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
338 soft_limit_byte_size_pooled_transactions_response:
339 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
340 soft_limit_byte_size_pooled_transactions_response_on_pack_request: DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
341 max_pending_pool_imports: DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS,
342 max_seen_tx_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
343 max_capacity_cache_txns_pending_fetch: DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
344 net_if: None,
345 tx_propagation_policy: TransactionPropagationKind::default()
346 }
347 }
348}
349
350#[derive(Debug, Clone, Args, PartialEq, Eq)]
352pub struct DiscoveryArgs {
353 #[arg(short, long, default_value_if("dev", "true", "true"))]
355 pub disable_discovery: bool,
356
357 #[arg(long, conflicts_with = "disable_discovery")]
359 pub disable_dns_discovery: bool,
360
361 #[arg(long, conflicts_with = "disable_discovery")]
363 pub disable_discv4_discovery: bool,
364
365 #[arg(long, conflicts_with = "disable_discovery")]
367 pub enable_discv5_discovery: bool,
368
369 #[arg(long, conflicts_with = "disable_discovery")]
371 pub disable_nat: bool,
372
373 #[arg(id = "discovery.addr", long = "discovery.addr", value_name = "DISCOVERY_ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
375 pub addr: IpAddr,
376
377 #[arg(id = "discovery.port", long = "discovery.port", value_name = "DISCOVERY_PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
379 pub port: u16,
380
381 #[arg(id = "discovery.v5.addr", long = "discovery.v5.addr", value_name = "DISCOVERY_V5_ADDR", default_value = None)]
384 pub discv5_addr: Option<Ipv4Addr>,
385
386 #[arg(id = "discovery.v5.addr.ipv6", long = "discovery.v5.addr.ipv6", value_name = "DISCOVERY_V5_ADDR_IPV6", default_value = None)]
389 pub discv5_addr_ipv6: Option<Ipv6Addr>,
390
391 #[arg(id = "discovery.v5.port", long = "discovery.v5.port", value_name = "DISCOVERY_V5_PORT",
394 default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
395 pub discv5_port: u16,
396
397 #[arg(id = "discovery.v5.port.ipv6", long = "discovery.v5.port.ipv6", value_name = "DISCOVERY_V5_PORT_IPV6",
400 default_value = None, default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
401 pub discv5_port_ipv6: u16,
402
403 #[arg(id = "discovery.v5.lookup-interval", long = "discovery.v5.lookup-interval", value_name = "DISCOVERY_V5_LOOKUP_INTERVAL", default_value_t = DEFAULT_SECONDS_LOOKUP_INTERVAL)]
406 pub discv5_lookup_interval: u64,
407
408 #[arg(id = "discovery.v5.bootstrap.lookup-interval", long = "discovery.v5.bootstrap.lookup-interval", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_INTERVAL",
411 default_value_t = DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL)]
412 pub discv5_bootstrap_lookup_interval: u64,
413
414 #[arg(id = "discovery.v5.bootstrap.lookup-countdown", long = "discovery.v5.bootstrap.lookup-countdown", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_COUNTDOWN",
416 default_value_t = DEFAULT_COUNT_BOOTSTRAP_LOOKUPS)]
417 pub discv5_bootstrap_lookup_countdown: u64,
418}
419
420impl DiscoveryArgs {
421 pub fn apply_to_builder<N>(
423 &self,
424 mut network_config_builder: NetworkConfigBuilder<N>,
425 rlpx_tcp_socket: SocketAddr,
426 boot_nodes: impl IntoIterator<Item = NodeRecord>,
427 ) -> NetworkConfigBuilder<N>
428 where
429 N: NetworkPrimitives,
430 {
431 if self.disable_discovery || self.disable_dns_discovery {
432 network_config_builder = network_config_builder.disable_dns_discovery();
433 }
434
435 if self.disable_discovery || self.disable_discv4_discovery {
436 network_config_builder = network_config_builder.disable_discv4_discovery();
437 }
438
439 if self.disable_nat {
440 network_config_builder = network_config_builder.disable_nat();
442 }
443
444 if !self.disable_discovery && self.enable_discv5_discovery {
445 network_config_builder = network_config_builder
446 .discovery_v5(self.discovery_v5_builder(rlpx_tcp_socket, boot_nodes));
447 }
448
449 network_config_builder
450 }
451
452 pub fn discovery_v5_builder(
454 &self,
455 rlpx_tcp_socket: SocketAddr,
456 boot_nodes: impl IntoIterator<Item = NodeRecord>,
457 ) -> reth_discv5::ConfigBuilder {
458 let Self {
459 discv5_addr,
460 discv5_addr_ipv6,
461 discv5_port,
462 discv5_port_ipv6,
463 discv5_lookup_interval,
464 discv5_bootstrap_lookup_interval,
465 discv5_bootstrap_lookup_countdown,
466 ..
467 } = self;
468
469 let discv5_addr_ipv4 = discv5_addr.or(match rlpx_tcp_socket {
471 SocketAddr::V4(addr) => Some(*addr.ip()),
472 SocketAddr::V6(_) => None,
473 });
474 let discv5_addr_ipv6 = discv5_addr_ipv6.or(match rlpx_tcp_socket {
475 SocketAddr::V4(_) => None,
476 SocketAddr::V6(addr) => Some(*addr.ip()),
477 });
478
479 reth_discv5::Config::builder(rlpx_tcp_socket)
480 .discv5_config(
481 reth_discv5::discv5::ConfigBuilder::new(ListenConfig::from_two_sockets(
482 discv5_addr_ipv4.map(|addr| SocketAddrV4::new(addr, *discv5_port)),
483 discv5_addr_ipv6.map(|addr| SocketAddrV6::new(addr, *discv5_port_ipv6, 0, 0)),
484 ))
485 .build(),
486 )
487 .add_unsigned_boot_nodes(boot_nodes)
488 .lookup_interval(*discv5_lookup_interval)
489 .bootstrap_lookup_interval(*discv5_bootstrap_lookup_interval)
490 .bootstrap_lookup_countdown(*discv5_bootstrap_lookup_countdown)
491 }
492
493 pub const fn with_unused_discovery_port(mut self) -> Self {
496 self.port = 0;
497 self
498 }
499
500 pub fn adjust_instance_ports(&mut self, instance: u16) {
506 debug_assert_ne!(instance, 0, "instance must be non-zero");
507 self.port += instance - 1;
508 self.discv5_port += instance - 1;
509 self.discv5_port_ipv6 += instance - 1;
510 }
511}
512
513impl Default for DiscoveryArgs {
514 fn default() -> Self {
515 Self {
516 disable_discovery: false,
517 disable_dns_discovery: false,
518 disable_discv4_discovery: false,
519 enable_discv5_discovery: false,
520 disable_nat: false,
521 addr: DEFAULT_DISCOVERY_ADDR,
522 port: DEFAULT_DISCOVERY_PORT,
523 discv5_addr: None,
524 discv5_addr_ipv6: None,
525 discv5_port: DEFAULT_DISCOVERY_V5_PORT,
526 discv5_port_ipv6: DEFAULT_DISCOVERY_V5_PORT,
527 discv5_lookup_interval: DEFAULT_SECONDS_LOOKUP_INTERVAL,
528 discv5_bootstrap_lookup_interval: DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL,
529 discv5_bootstrap_lookup_countdown: DEFAULT_COUNT_BOOTSTRAP_LOOKUPS,
530 }
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use clap::Parser;
538 #[derive(Parser)]
540 struct CommandParser<T: Args> {
541 #[command(flatten)]
542 args: T,
543 }
544
545 #[test]
546 fn parse_nat_args() {
547 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "none"]).args;
548 assert_eq!(args.nat, NatResolver::None);
549
550 let args =
551 CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "extip:0.0.0.0"]).args;
552 assert_eq!(args.nat, NatResolver::ExternalIp("0.0.0.0".parse().unwrap()));
553 }
554
555 #[test]
556 fn parse_peer_args() {
557 let args =
558 CommandParser::<NetworkArgs>::parse_from(["reth", "--max-outbound-peers", "50"]).args;
559 assert_eq!(args.max_outbound_peers, Some(50));
560 assert_eq!(args.max_inbound_peers, None);
561
562 let args = CommandParser::<NetworkArgs>::parse_from([
563 "reth",
564 "--max-outbound-peers",
565 "75",
566 "--max-inbound-peers",
567 "15",
568 ])
569 .args;
570 assert_eq!(args.max_outbound_peers, Some(75));
571 assert_eq!(args.max_inbound_peers, Some(15));
572 }
573
574 #[test]
575 fn parse_trusted_peer_args() {
576 let args =
577 CommandParser::<NetworkArgs>::parse_from([
578 "reth",
579 "--trusted-peers",
580 "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303,enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303"
581 ])
582 .args;
583
584 assert_eq!(
585 args.trusted_peers,
586 vec![
587 "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303".parse().unwrap(),
588 "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303".parse().unwrap()
589 ]
590 );
591 }
592
593 #[test]
594 fn parse_retry_strategy_args() {
595 let tests = vec![0, 10];
596
597 for retries in tests {
598 let args = CommandParser::<NetworkArgs>::parse_from([
599 "reth",
600 "--dns-retries",
601 retries.to_string().as_str(),
602 ])
603 .args;
604
605 assert_eq!(args.dns_retries, retries);
606 }
607 }
608
609 #[test]
610 fn network_args_default_sanity_test() {
611 let default_args = NetworkArgs::default();
612 let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
613
614 assert_eq!(args, default_args);
615 }
616}