1use std::{
4 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
5 ops::Not,
6 path::PathBuf,
7};
8
9use crate::version::version_metadata;
10use clap::Args;
11use reth_chainspec::EthChainSpec;
12use reth_config::Config;
13use reth_discv4::{NodeRecord, DEFAULT_DISCOVERY_ADDR, DEFAULT_DISCOVERY_PORT};
14use reth_discv5::{
15 discv5::ListenConfig, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_PORT,
16 DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL,
17};
18use reth_net_nat::{NatResolver, DEFAULT_NET_IF_NAME};
19use reth_network::{
20 transactions::{
21 config::TransactionPropagationKind,
22 constants::{
23 tx_fetcher::{
24 DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
25 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
26 },
27 tx_manager::{
28 DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
29 },
30 },
31 TransactionFetcherConfig, TransactionPropagationMode, TransactionsManagerConfig,
32 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
33 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
34 },
35 HelloMessageWithProtocols, NetworkConfigBuilder, NetworkPrimitives, SessionsConfig,
36};
37use reth_network_peers::{mainnet_nodes, TrustedPeer};
38use secp256k1::SecretKey;
39use tracing::error;
40
41#[derive(Debug, Clone, Args, PartialEq, Eq)]
43#[command(next_help_heading = "Networking")]
44pub struct NetworkArgs {
45 #[command(flatten)]
47 pub discovery: DiscoveryArgs,
48
49 #[expect(clippy::doc_markdown)]
50 #[arg(long, value_delimiter = ',')]
54 pub trusted_peers: Vec<TrustedPeer>,
55
56 #[arg(long)]
58 pub trusted_only: bool,
59
60 #[arg(long, value_delimiter = ',')]
64 pub bootnodes: Option<Vec<TrustedPeer>>,
65
66 #[arg(long, default_value_t = 0)]
68 pub dns_retries: usize,
69
70 #[arg(long, value_name = "FILE", verbatim_doc_comment, conflicts_with = "no_persist_peers")]
73 pub peers_file: Option<PathBuf>,
74
75 #[arg(long, value_name = "IDENTITY", default_value = version_metadata().p2p_client_version.as_ref())]
77 pub identity: String,
78
79 #[arg(long, value_name = "PATH")]
84 pub p2p_secret_key: Option<PathBuf>,
85
86 #[arg(long, verbatim_doc_comment)]
88 pub no_persist_peers: bool,
89
90 #[arg(long, default_value = "any")]
92 pub nat: NatResolver,
93
94 #[arg(long = "addr", value_name = "ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
96 pub addr: IpAddr,
97
98 #[arg(long = "port", value_name = "PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
100 pub port: u16,
101
102 #[arg(long)]
104 pub max_outbound_peers: Option<usize>,
105
106 #[arg(long)]
108 pub max_inbound_peers: Option<usize>,
109
110 #[arg(long = "max-tx-reqs", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS, verbatim_doc_comment)]
112 pub max_concurrent_tx_requests: u32,
113
114 #[arg(long = "max-tx-reqs-peer", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER, verbatim_doc_comment)]
116 pub max_concurrent_tx_requests_per_peer: u8,
117
118 #[arg(long = "max-seen-tx-history", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER, verbatim_doc_comment)]
122 pub max_seen_tx_history: u32,
123
124 #[arg(long = "max-pending-imports", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, verbatim_doc_comment)]
125 pub max_pending_pool_imports: usize,
127
128 #[arg(long = "pooled-tx-response-soft-limit", value_name = "BYTES", default_value_t = SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE, verbatim_doc_comment)]
132 pub soft_limit_byte_size_pooled_transactions_response: usize,
133
134 #[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)]
146 pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
147
148 #[arg(long = "max-tx-pending-fetch", value_name = "COUNT", default_value_t = DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, verbatim_doc_comment)]
150 pub max_capacity_cache_txns_pending_fetch: u32,
151
152 #[arg(long = "net-if.experimental", conflicts_with = "addr", value_name = "IF_NAME")]
156 pub net_if: Option<String>,
157
158 #[arg(long = "tx-propagation-policy", default_value_t = TransactionPropagationKind::All)]
162 pub tx_propagation_policy: TransactionPropagationKind,
163
164 #[arg(long = "disable-tx-gossip")]
169 pub disable_tx_gossip: bool,
170
171 #[arg(
176 long = "tx-propagation-mode",
177 default_value = "sqrt",
178 help = "Transaction propagation mode (sqrt, all, max:<number>)"
179 )]
180 pub propagation_mode: TransactionPropagationMode,
181}
182
183impl NetworkArgs {
184 pub fn resolved_addr(&self) -> IpAddr {
186 if let Some(ref if_name) = self.net_if {
187 let if_name = if if_name.is_empty() { DEFAULT_NET_IF_NAME } else { if_name };
188 return match reth_net_nat::net_if::resolve_net_if_ip(if_name) {
189 Ok(addr) => addr,
190 Err(err) => {
191 error!(target: "reth::cli",
192 if_name,
193 %err,
194 "Failed to read network interface IP"
195 );
196
197 DEFAULT_DISCOVERY_ADDR
198 }
199 };
200 }
201
202 self.addr
203 }
204
205 pub fn resolved_bootnodes(&self) -> Option<Vec<NodeRecord>> {
207 self.bootnodes.clone().map(|bootnodes| {
208 bootnodes.into_iter().filter_map(|node| node.resolve_blocking().ok()).collect()
209 })
210 }
211 pub const fn transactions_manager_config(&self) -> TransactionsManagerConfig {
213 TransactionsManagerConfig {
214 transaction_fetcher_config: TransactionFetcherConfig::new(
215 self.max_concurrent_tx_requests,
216 self.max_concurrent_tx_requests_per_peer,
217 self.soft_limit_byte_size_pooled_transactions_response,
218 self.soft_limit_byte_size_pooled_transactions_response_on_pack_request,
219 self.max_capacity_cache_txns_pending_fetch,
220 ),
221 max_transactions_seen_by_peer_history: self.max_seen_tx_history,
222 propagation_mode: self.propagation_mode,
223 }
224 }
225
226 pub fn network_config<N: NetworkPrimitives>(
238 &self,
239 config: &Config,
240 chain_spec: impl EthChainSpec,
241 secret_key: SecretKey,
242 default_peers_file: PathBuf,
243 ) -> NetworkConfigBuilder<N> {
244 let addr = self.resolved_addr();
245 let chain_bootnodes = self
246 .resolved_bootnodes()
247 .unwrap_or_else(|| chain_spec.bootnodes().unwrap_or_else(mainnet_nodes));
248 let peers_file = self.peers_file.clone().unwrap_or(default_peers_file);
249
250 let peers_config = config
252 .peers
253 .clone()
254 .with_max_inbound_opt(self.max_inbound_peers)
255 .with_max_outbound_opt(self.max_outbound_peers);
256
257 NetworkConfigBuilder::<N>::new(secret_key)
259 .peer_config(config.peers_config_with_basic_nodes_from_file(
260 self.persistent_peers_file(peers_file).as_deref(),
261 ))
262 .external_ip_resolver(self.nat)
263 .sessions_config(
264 SessionsConfig::default().with_upscaled_event_buffer(peers_config.max_peers()),
265 )
266 .peer_config(peers_config)
267 .boot_nodes(chain_bootnodes.clone())
268 .transactions_manager_config(self.transactions_manager_config())
269 .apply(|builder| {
271 let peer_id = builder.get_peer_id();
272 builder.hello_message(
273 HelloMessageWithProtocols::builder(peer_id)
274 .client_version(&self.identity)
275 .build(),
276 )
277 })
278 .apply(|builder| {
280 let rlpx_socket = (addr, self.port).into();
281 self.discovery.apply_to_builder(builder, rlpx_socket, chain_bootnodes)
282 })
283 .listener_addr(SocketAddr::new(
284 addr, self.port,
286 ))
287 .discovery_addr(SocketAddr::new(
288 self.discovery.addr,
289 self.discovery.port,
291 ))
292 .disable_tx_gossip(self.disable_tx_gossip)
293 }
294
295 pub fn persistent_peers_file(&self, peers_file: PathBuf) -> Option<PathBuf> {
297 self.no_persist_peers.not().then_some(peers_file)
298 }
299
300 pub const fn with_unused_p2p_port(mut self) -> Self {
303 self.port = 0;
304 self
305 }
306
307 pub const fn with_unused_ports(mut self) -> Self {
310 self = self.with_unused_p2p_port();
311 self.discovery = self.discovery.with_unused_discovery_port();
312 self
313 }
314
315 pub fn adjust_instance_ports(&mut self, instance: Option<u16>) {
321 if let Some(instance) = instance {
322 debug_assert_ne!(instance, 0, "instance must be non-zero");
323 self.port += instance - 1;
324 self.discovery.adjust_instance_ports(instance);
325 }
326 }
327
328 pub async fn resolve_trusted_peers(&self) -> Result<Vec<NodeRecord>, std::io::Error> {
330 futures::future::try_join_all(
331 self.trusted_peers.iter().map(|peer| async move { peer.resolve().await }),
332 )
333 .await
334 }
335}
336
337impl Default for NetworkArgs {
338 fn default() -> Self {
339 Self {
340 discovery: DiscoveryArgs::default(),
341 trusted_peers: vec![],
342 trusted_only: false,
343 bootnodes: None,
344 dns_retries: 0,
345 peers_file: None,
346 identity: version_metadata().p2p_client_version.to_string(),
347 p2p_secret_key: None,
348 no_persist_peers: false,
349 nat: NatResolver::Any,
350 addr: DEFAULT_DISCOVERY_ADDR,
351 port: DEFAULT_DISCOVERY_PORT,
352 max_outbound_peers: None,
353 max_inbound_peers: None,
354 max_concurrent_tx_requests: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
355 max_concurrent_tx_requests_per_peer: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
356 soft_limit_byte_size_pooled_transactions_response:
357 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
358 soft_limit_byte_size_pooled_transactions_response_on_pack_request: DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
359 max_pending_pool_imports: DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS,
360 max_seen_tx_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
361 max_capacity_cache_txns_pending_fetch: DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
362 net_if: None,
363 tx_propagation_policy: TransactionPropagationKind::default(),
364 disable_tx_gossip: false,
365 propagation_mode: TransactionPropagationMode::Sqrt,
366 }
367 }
368}
369
370#[derive(Debug, Clone, Args, PartialEq, Eq)]
372pub struct DiscoveryArgs {
373 #[arg(short, long, default_value_if("dev", "true", "true"))]
375 pub disable_discovery: bool,
376
377 #[arg(long, conflicts_with = "disable_discovery")]
379 pub disable_dns_discovery: bool,
380
381 #[arg(long, conflicts_with = "disable_discovery")]
383 pub disable_discv4_discovery: bool,
384
385 #[arg(long, conflicts_with = "disable_discovery")]
387 pub enable_discv5_discovery: bool,
388
389 #[arg(long, conflicts_with = "disable_discovery")]
391 pub disable_nat: bool,
392
393 #[arg(id = "discovery.addr", long = "discovery.addr", value_name = "DISCOVERY_ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
395 pub addr: IpAddr,
396
397 #[arg(id = "discovery.port", long = "discovery.port", value_name = "DISCOVERY_PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
399 pub port: u16,
400
401 #[arg(id = "discovery.v5.addr", long = "discovery.v5.addr", value_name = "DISCOVERY_V5_ADDR", default_value = None)]
404 pub discv5_addr: Option<Ipv4Addr>,
405
406 #[arg(id = "discovery.v5.addr.ipv6", long = "discovery.v5.addr.ipv6", value_name = "DISCOVERY_V5_ADDR_IPV6", default_value = None)]
409 pub discv5_addr_ipv6: Option<Ipv6Addr>,
410
411 #[arg(id = "discovery.v5.port", long = "discovery.v5.port", value_name = "DISCOVERY_V5_PORT",
414 default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
415 pub discv5_port: u16,
416
417 #[arg(id = "discovery.v5.port.ipv6", long = "discovery.v5.port.ipv6", value_name = "DISCOVERY_V5_PORT_IPV6",
420 default_value = None, default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
421 pub discv5_port_ipv6: u16,
422
423 #[arg(id = "discovery.v5.lookup-interval", long = "discovery.v5.lookup-interval", value_name = "DISCOVERY_V5_LOOKUP_INTERVAL", default_value_t = DEFAULT_SECONDS_LOOKUP_INTERVAL)]
426 pub discv5_lookup_interval: u64,
427
428 #[arg(id = "discovery.v5.bootstrap.lookup-interval", long = "discovery.v5.bootstrap.lookup-interval", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_INTERVAL",
431 default_value_t = DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL)]
432 pub discv5_bootstrap_lookup_interval: u64,
433
434 #[arg(id = "discovery.v5.bootstrap.lookup-countdown", long = "discovery.v5.bootstrap.lookup-countdown", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_COUNTDOWN",
436 default_value_t = DEFAULT_COUNT_BOOTSTRAP_LOOKUPS)]
437 pub discv5_bootstrap_lookup_countdown: u64,
438}
439
440impl DiscoveryArgs {
441 pub fn apply_to_builder<N>(
443 &self,
444 mut network_config_builder: NetworkConfigBuilder<N>,
445 rlpx_tcp_socket: SocketAddr,
446 boot_nodes: impl IntoIterator<Item = NodeRecord>,
447 ) -> NetworkConfigBuilder<N>
448 where
449 N: NetworkPrimitives,
450 {
451 if self.disable_discovery || self.disable_dns_discovery {
452 network_config_builder = network_config_builder.disable_dns_discovery();
453 }
454
455 if self.disable_discovery || self.disable_discv4_discovery {
456 network_config_builder = network_config_builder.disable_discv4_discovery();
457 }
458
459 if self.disable_nat {
460 network_config_builder = network_config_builder.disable_nat();
462 }
463
464 if self.should_enable_discv5() {
465 network_config_builder = network_config_builder
466 .discovery_v5(self.discovery_v5_builder(rlpx_tcp_socket, boot_nodes));
467 }
468
469 network_config_builder
470 }
471
472 pub fn discovery_v5_builder(
474 &self,
475 rlpx_tcp_socket: SocketAddr,
476 boot_nodes: impl IntoIterator<Item = NodeRecord>,
477 ) -> reth_discv5::ConfigBuilder {
478 let Self {
479 discv5_addr,
480 discv5_addr_ipv6,
481 discv5_port,
482 discv5_port_ipv6,
483 discv5_lookup_interval,
484 discv5_bootstrap_lookup_interval,
485 discv5_bootstrap_lookup_countdown,
486 ..
487 } = self;
488
489 let discv5_addr_ipv4 = discv5_addr.or(match rlpx_tcp_socket {
491 SocketAddr::V4(addr) => Some(*addr.ip()),
492 SocketAddr::V6(_) => None,
493 });
494 let discv5_addr_ipv6 = discv5_addr_ipv6.or(match rlpx_tcp_socket {
495 SocketAddr::V4(_) => None,
496 SocketAddr::V6(addr) => Some(*addr.ip()),
497 });
498
499 reth_discv5::Config::builder(rlpx_tcp_socket)
500 .discv5_config(
501 reth_discv5::discv5::ConfigBuilder::new(ListenConfig::from_two_sockets(
502 discv5_addr_ipv4.map(|addr| SocketAddrV4::new(addr, *discv5_port)),
503 discv5_addr_ipv6.map(|addr| SocketAddrV6::new(addr, *discv5_port_ipv6, 0, 0)),
504 ))
505 .build(),
506 )
507 .add_unsigned_boot_nodes(boot_nodes)
508 .lookup_interval(*discv5_lookup_interval)
509 .bootstrap_lookup_interval(*discv5_bootstrap_lookup_interval)
510 .bootstrap_lookup_countdown(*discv5_bootstrap_lookup_countdown)
511 }
512
513 const fn should_enable_discv5(&self) -> bool {
515 if self.disable_discovery {
516 return false;
517 }
518
519 self.enable_discv5_discovery ||
520 self.discv5_addr.is_some() ||
521 self.discv5_addr_ipv6.is_some()
522 }
523
524 pub const fn with_unused_discovery_port(mut self) -> Self {
527 self.port = 0;
528 self
529 }
530
531 pub fn adjust_instance_ports(&mut self, instance: u16) {
537 debug_assert_ne!(instance, 0, "instance must be non-zero");
538 self.port += instance - 1;
539 self.discv5_port += instance - 1;
540 self.discv5_port_ipv6 += instance - 1;
541 }
542}
543
544impl Default for DiscoveryArgs {
545 fn default() -> Self {
546 Self {
547 disable_discovery: false,
548 disable_dns_discovery: false,
549 disable_discv4_discovery: false,
550 enable_discv5_discovery: false,
551 disable_nat: false,
552 addr: DEFAULT_DISCOVERY_ADDR,
553 port: DEFAULT_DISCOVERY_PORT,
554 discv5_addr: None,
555 discv5_addr_ipv6: None,
556 discv5_port: DEFAULT_DISCOVERY_V5_PORT,
557 discv5_port_ipv6: DEFAULT_DISCOVERY_V5_PORT,
558 discv5_lookup_interval: DEFAULT_SECONDS_LOOKUP_INTERVAL,
559 discv5_bootstrap_lookup_interval: DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL,
560 discv5_bootstrap_lookup_countdown: DEFAULT_COUNT_BOOTSTRAP_LOOKUPS,
561 }
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use clap::Parser;
569 #[derive(Parser)]
571 struct CommandParser<T: Args> {
572 #[command(flatten)]
573 args: T,
574 }
575
576 #[test]
577 fn parse_nat_args() {
578 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "none"]).args;
579 assert_eq!(args.nat, NatResolver::None);
580
581 let args =
582 CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "extip:0.0.0.0"]).args;
583 assert_eq!(args.nat, NatResolver::ExternalIp("0.0.0.0".parse().unwrap()));
584 }
585
586 #[test]
587 fn parse_peer_args() {
588 let args =
589 CommandParser::<NetworkArgs>::parse_from(["reth", "--max-outbound-peers", "50"]).args;
590 assert_eq!(args.max_outbound_peers, Some(50));
591 assert_eq!(args.max_inbound_peers, None);
592
593 let args = CommandParser::<NetworkArgs>::parse_from([
594 "reth",
595 "--max-outbound-peers",
596 "75",
597 "--max-inbound-peers",
598 "15",
599 ])
600 .args;
601 assert_eq!(args.max_outbound_peers, Some(75));
602 assert_eq!(args.max_inbound_peers, Some(15));
603 }
604
605 #[test]
606 fn parse_trusted_peer_args() {
607 let args =
608 CommandParser::<NetworkArgs>::parse_from([
609 "reth",
610 "--trusted-peers",
611 "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303,enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303"
612 ])
613 .args;
614
615 assert_eq!(
616 args.trusted_peers,
617 vec![
618 "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303".parse().unwrap(),
619 "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303".parse().unwrap()
620 ]
621 );
622 }
623
624 #[test]
625 fn parse_retry_strategy_args() {
626 let tests = vec![0, 10];
627
628 for retries in tests {
629 let args = CommandParser::<NetworkArgs>::parse_from([
630 "reth",
631 "--dns-retries",
632 retries.to_string().as_str(),
633 ])
634 .args;
635
636 assert_eq!(args.dns_retries, retries);
637 }
638 }
639
640 #[test]
641 fn parse_disable_tx_gossip_args() {
642 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--disable-tx-gossip"]).args;
643 assert!(args.disable_tx_gossip);
644 }
645
646 #[test]
647 fn network_args_default_sanity_test() {
648 let default_args = NetworkArgs::default();
649 let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
650
651 assert_eq!(args, default_args);
652 }
653}