1use alloy_eips::BlockNumHash;
4use alloy_primitives::B256;
5use std::{
6 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
7 ops::Not,
8 path::PathBuf,
9};
10
11use crate::version::version_metadata;
12use clap::Args;
13use reth_chainspec::EthChainSpec;
14use reth_cli_util::{get_secret_key, load_secret_key::SecretKeyError};
15use reth_config::Config;
16use reth_discv4::{NodeRecord, DEFAULT_DISCOVERY_ADDR, DEFAULT_DISCOVERY_PORT};
17use reth_discv5::{
18 discv5::ListenConfig, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_PORT,
19 DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL,
20};
21use reth_net_banlist::IpFilter;
22use reth_net_nat::{NatResolver, DEFAULT_NET_IF_NAME};
23use reth_network::{
24 transactions::{
25 config::{TransactionIngressPolicy, TransactionPropagationKind},
26 constants::{
27 tx_fetcher::{
28 DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
29 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
30 },
31 tx_manager::{
32 DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
33 },
34 },
35 TransactionFetcherConfig, TransactionPropagationMode, TransactionsManagerConfig,
36 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
37 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
38 },
39 HelloMessageWithProtocols, NetworkConfigBuilder, NetworkPrimitives,
40};
41use reth_network_peers::{mainnet_nodes, TrustedPeer};
42use reth_tasks::Runtime;
43use secp256k1::SecretKey;
44use std::str::FromStr;
45use tracing::error;
46
47#[derive(Debug, Clone, Args, PartialEq, Eq)]
49#[command(next_help_heading = "Networking")]
50pub struct NetworkArgs {
51 #[command(flatten)]
53 pub discovery: DiscoveryArgs,
54
55 #[expect(clippy::doc_markdown)]
56 #[arg(long, value_delimiter = ',')]
60 pub trusted_peers: Vec<TrustedPeer>,
61
62 #[arg(long)]
64 pub trusted_only: bool,
65
66 #[arg(long, value_delimiter = ',')]
70 pub bootnodes: Option<Vec<TrustedPeer>>,
71
72 #[arg(long, default_value_t = 0)]
74 pub dns_retries: usize,
75
76 #[arg(long, value_name = "FILE", verbatim_doc_comment, conflicts_with = "no_persist_peers")]
79 pub peers_file: Option<PathBuf>,
80
81 #[arg(long, value_name = "IDENTITY", default_value = version_metadata().p2p_client_version.as_ref())]
83 pub identity: String,
84
85 #[arg(long, value_name = "PATH", conflicts_with = "p2p_secret_key_hex")]
90 pub p2p_secret_key: Option<PathBuf>,
91
92 #[arg(long, value_name = "HEX", conflicts_with = "p2p_secret_key")]
97 pub p2p_secret_key_hex: Option<B256>,
98
99 #[arg(long, verbatim_doc_comment)]
101 pub no_persist_peers: bool,
102
103 #[arg(long, default_value = "any")]
105 pub nat: NatResolver,
106
107 #[arg(long = "addr", value_name = "ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
109 pub addr: IpAddr,
110
111 #[arg(long = "port", value_name = "PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
113 pub port: u16,
114
115 #[arg(long)]
117 pub max_outbound_peers: Option<usize>,
118
119 #[arg(long)]
121 pub max_inbound_peers: Option<usize>,
122
123 #[arg(
128 long,
129 value_name = "COUNT",
130 conflicts_with = "max_outbound_peers",
131 conflicts_with = "max_inbound_peers"
132 )]
133 pub max_peers: Option<usize>,
134
135 #[arg(long = "max-tx-reqs", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS, verbatim_doc_comment)]
137 pub max_concurrent_tx_requests: u32,
138
139 #[arg(long = "max-tx-reqs-peer", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER, verbatim_doc_comment)]
141 pub max_concurrent_tx_requests_per_peer: u8,
142
143 #[arg(long = "max-seen-tx-history", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER, verbatim_doc_comment)]
147 pub max_seen_tx_history: u32,
148
149 #[arg(long = "max-pending-imports", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, verbatim_doc_comment)]
150 pub max_pending_pool_imports: usize,
152
153 #[arg(long = "pooled-tx-response-soft-limit", value_name = "BYTES", default_value_t = SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE, verbatim_doc_comment)]
157 pub soft_limit_byte_size_pooled_transactions_response: usize,
158
159 #[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)]
171 pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
172
173 #[arg(long = "max-tx-pending-fetch", value_name = "COUNT", default_value_t = DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, verbatim_doc_comment)]
175 pub max_capacity_cache_txns_pending_fetch: u32,
176
177 #[arg(long = "net-if.experimental", conflicts_with = "addr", value_name = "IF_NAME")]
181 pub net_if: Option<String>,
182
183 #[arg(long = "tx-propagation-policy", default_value_t = TransactionPropagationKind::All)]
187 pub tx_propagation_policy: TransactionPropagationKind,
188
189 #[arg(long = "tx-ingress-policy", default_value_t = TransactionIngressPolicy::All)]
193 pub tx_ingress_policy: TransactionIngressPolicy,
194
195 #[arg(long = "disable-tx-gossip")]
200 pub disable_tx_gossip: bool,
201
202 #[arg(
207 long = "tx-propagation-mode",
208 default_value = "sqrt",
209 help = "Transaction propagation mode (sqrt, all, max:<number>)"
210 )]
211 pub propagation_mode: TransactionPropagationMode,
212
213 #[arg(long = "required-block-hashes", value_delimiter = ',', value_parser = parse_block_num_hash)]
217 pub required_block_hashes: Vec<BlockNumHash>,
218
219 #[arg(long)]
221 pub network_id: Option<u64>,
222
223 #[arg(long, value_name = "NETRESTRICT")]
230 pub netrestrict: Option<String>,
231
232 #[arg(long)]
238 pub enforce_enr_fork_id: bool,
239}
240
241impl NetworkArgs {
242 pub fn resolved_addr(&self) -> IpAddr {
244 if let Some(ref if_name) = self.net_if {
245 let if_name = if if_name.is_empty() { DEFAULT_NET_IF_NAME } else { if_name };
246 return match reth_net_nat::net_if::resolve_net_if_ip(if_name) {
247 Ok(addr) => addr,
248 Err(err) => {
249 error!(target: "reth::cli",
250 if_name,
251 %err,
252 "Failed to read network interface IP"
253 );
254
255 DEFAULT_DISCOVERY_ADDR
256 }
257 };
258 }
259
260 self.addr
261 }
262
263 pub fn resolved_bootnodes(&self) -> Option<Vec<NodeRecord>> {
265 self.bootnodes.clone().map(|bootnodes| {
266 bootnodes.into_iter().filter_map(|node| node.resolve_blocking().ok()).collect()
267 })
268 }
269
270 pub fn resolved_max_inbound_peers(&self) -> Option<usize> {
272 if let Some(max_peers) = self.max_peers {
273 if max_peers == 0 {
274 Some(0)
275 } else {
276 let outbound = (max_peers / 3).max(1);
277 Some(max_peers.saturating_sub(outbound))
278 }
279 } else {
280 self.max_inbound_peers
281 }
282 }
283
284 pub fn resolved_max_outbound_peers(&self) -> Option<usize> {
286 if let Some(max_peers) = self.max_peers {
287 if max_peers == 0 {
288 Some(0)
289 } else {
290 Some((max_peers / 3).max(1))
291 }
292 } else {
293 self.max_outbound_peers
294 }
295 }
296
297 pub const fn transactions_manager_config(&self) -> TransactionsManagerConfig {
299 TransactionsManagerConfig {
300 transaction_fetcher_config: TransactionFetcherConfig::new(
301 self.max_concurrent_tx_requests,
302 self.max_concurrent_tx_requests_per_peer,
303 self.soft_limit_byte_size_pooled_transactions_response,
304 self.soft_limit_byte_size_pooled_transactions_response_on_pack_request,
305 self.max_capacity_cache_txns_pending_fetch,
306 ),
307 max_transactions_seen_by_peer_history: self.max_seen_tx_history,
308 propagation_mode: self.propagation_mode,
309 ingress_policy: self.tx_ingress_policy,
310 }
311 }
312
313 pub fn network_config<N: NetworkPrimitives>(
325 &self,
326 config: &Config,
327 chain_spec: impl EthChainSpec,
328 secret_key: SecretKey,
329 default_peers_file: PathBuf,
330 executor: Runtime,
331 ) -> NetworkConfigBuilder<N> {
332 let addr = self.resolved_addr();
333 let chain_bootnodes = self
334 .resolved_bootnodes()
335 .unwrap_or_else(|| chain_spec.bootnodes().unwrap_or_else(mainnet_nodes));
336 let peers_file = self.peers_file.clone().unwrap_or(default_peers_file);
337
338 let ip_filter = self.ip_filter().unwrap_or_default();
340 let peers_config = config
341 .peers_config_with_basic_nodes_from_file(
342 self.persistent_peers_file(peers_file).as_deref(),
343 )
344 .with_max_inbound_opt(self.resolved_max_inbound_peers())
345 .with_max_outbound_opt(self.resolved_max_outbound_peers())
346 .with_ip_filter(ip_filter)
347 .with_enforce_enr_fork_id(self.enforce_enr_fork_id);
348
349 NetworkConfigBuilder::<N>::new(secret_key, executor)
351 .external_ip_resolver(self.nat.clone())
352 .sessions_config(
353 config.sessions.clone().with_upscaled_event_buffer(peers_config.max_peers()),
354 )
355 .peer_config(peers_config)
356 .boot_nodes(chain_bootnodes.clone())
357 .transactions_manager_config(self.transactions_manager_config())
358 .apply(|builder| {
360 let peer_id = builder.get_peer_id();
361 builder.hello_message(
362 HelloMessageWithProtocols::builder(peer_id)
363 .client_version(&self.identity)
364 .build(),
365 )
366 })
367 .apply(|builder| {
369 let rlpx_socket = (addr, self.port).into();
370 self.discovery.apply_to_builder(builder, rlpx_socket, chain_bootnodes)
371 })
372 .listener_addr(SocketAddr::new(
373 addr, self.port,
375 ))
376 .discovery_addr(SocketAddr::new(
377 self.discovery.addr,
378 self.discovery.port,
380 ))
381 .disable_tx_gossip(self.disable_tx_gossip)
382 .required_block_hashes(self.required_block_hashes.clone())
383 .network_id(self.network_id)
384 }
385
386 pub fn persistent_peers_file(&self, peers_file: PathBuf) -> Option<PathBuf> {
388 self.no_persist_peers.not().then_some(peers_file)
389 }
390
391 pub const fn with_discovery(mut self, discovery: DiscoveryArgs) -> Self {
393 self.discovery = discovery;
394 self
395 }
396
397 pub const fn with_unused_p2p_port(mut self) -> Self {
400 self.port = 0;
401 self
402 }
403
404 pub const fn with_unused_ports(mut self) -> Self {
407 self = self.with_unused_p2p_port();
408 self.discovery = self.discovery.with_unused_discovery_port();
409 self
410 }
411
412 pub fn with_nat_resolver(mut self, nat: NatResolver) -> Self {
414 self.nat = nat;
415 self
416 }
417
418 pub fn adjust_instance_ports(&mut self, instance: Option<u16>) {
424 if let Some(instance) = instance {
425 debug_assert_ne!(instance, 0, "instance must be non-zero");
426 self.port += instance - 1;
427 self.discovery.adjust_instance_ports(instance);
428 }
429 }
430
431 pub async fn resolve_trusted_peers(&self) -> Result<Vec<NodeRecord>, std::io::Error> {
433 futures::future::try_join_all(
434 self.trusted_peers.iter().map(|peer| async move { peer.resolve().await }),
435 )
436 .await
437 }
438
439 pub fn secret_key(
445 &self,
446 default_secret_key_path: PathBuf,
447 ) -> Result<SecretKey, SecretKeyError> {
448 if let Some(b256) = &self.p2p_secret_key_hex {
449 SecretKey::from_slice(b256.as_slice()).map_err(SecretKeyError::SecretKeyDecodeError)
451 } else {
452 let secret_key_path = self.p2p_secret_key.clone().unwrap_or(default_secret_key_path);
454 get_secret_key(&secret_key_path)
455 }
456 }
457
458 pub fn ip_filter(&self) -> Result<IpFilter, ipnet::AddrParseError> {
462 if let Some(netrestrict) = &self.netrestrict {
463 IpFilter::from_cidr_string(netrestrict)
464 } else {
465 Ok(IpFilter::allow_all())
466 }
467 }
468}
469
470impl Default for NetworkArgs {
471 fn default() -> Self {
472 Self {
473 discovery: DiscoveryArgs::default(),
474 trusted_peers: vec![],
475 trusted_only: false,
476 bootnodes: None,
477 dns_retries: 0,
478 peers_file: None,
479 identity: version_metadata().p2p_client_version.to_string(),
480 p2p_secret_key: None,
481 p2p_secret_key_hex: None,
482 no_persist_peers: false,
483 nat: NatResolver::Any,
484 addr: DEFAULT_DISCOVERY_ADDR,
485 port: DEFAULT_DISCOVERY_PORT,
486 max_outbound_peers: None,
487 max_inbound_peers: None,
488 max_peers: None,
489 max_concurrent_tx_requests: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
490 max_concurrent_tx_requests_per_peer: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
491 soft_limit_byte_size_pooled_transactions_response:
492 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
493 soft_limit_byte_size_pooled_transactions_response_on_pack_request: DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
494 max_pending_pool_imports: DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS,
495 max_seen_tx_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
496 max_capacity_cache_txns_pending_fetch: DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
497 net_if: None,
498 tx_propagation_policy: TransactionPropagationKind::default(),
499 tx_ingress_policy: TransactionIngressPolicy::default(),
500 disable_tx_gossip: false,
501 propagation_mode: TransactionPropagationMode::Sqrt,
502 required_block_hashes: vec![],
503 network_id: None,
504 netrestrict: None,
505 enforce_enr_fork_id: false,
506 }
507 }
508}
509
510#[derive(Debug, Clone, Args, PartialEq, Eq)]
512pub struct DiscoveryArgs {
513 #[arg(short, long, default_value_if("dev", "true", "true"))]
515 pub disable_discovery: bool,
516
517 #[arg(long, conflicts_with = "disable_discovery")]
519 pub disable_dns_discovery: bool,
520
521 #[arg(long, conflicts_with = "disable_discovery")]
523 pub disable_discv4_discovery: bool,
524
525 #[arg(long, conflicts_with = "disable_discovery")]
527 pub enable_discv5_discovery: bool,
528
529 #[arg(long, conflicts_with = "disable_discovery")]
531 pub disable_nat: bool,
532
533 #[arg(id = "discovery.addr", long = "discovery.addr", value_name = "DISCOVERY_ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
535 pub addr: IpAddr,
536
537 #[arg(id = "discovery.port", long = "discovery.port", value_name = "DISCOVERY_PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
539 pub port: u16,
540
541 #[arg(id = "discovery.v5.addr", long = "discovery.v5.addr", value_name = "DISCOVERY_V5_ADDR", default_value = None)]
544 pub discv5_addr: Option<Ipv4Addr>,
545
546 #[arg(id = "discovery.v5.addr.ipv6", long = "discovery.v5.addr.ipv6", value_name = "DISCOVERY_V5_ADDR_IPV6", default_value = None)]
549 pub discv5_addr_ipv6: Option<Ipv6Addr>,
550
551 #[arg(id = "discovery.v5.port", long = "discovery.v5.port", value_name = "DISCOVERY_V5_PORT",
554 default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
555 pub discv5_port: u16,
556
557 #[arg(id = "discovery.v5.port.ipv6", long = "discovery.v5.port.ipv6", value_name = "DISCOVERY_V5_PORT_IPV6",
560 default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
561 pub discv5_port_ipv6: u16,
562
563 #[arg(id = "discovery.v5.lookup-interval", long = "discovery.v5.lookup-interval", value_name = "DISCOVERY_V5_LOOKUP_INTERVAL", default_value_t = DEFAULT_SECONDS_LOOKUP_INTERVAL)]
566 pub discv5_lookup_interval: u64,
567
568 #[arg(id = "discovery.v5.bootstrap.lookup-interval", long = "discovery.v5.bootstrap.lookup-interval", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_INTERVAL",
571 default_value_t = DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL)]
572 pub discv5_bootstrap_lookup_interval: u64,
573
574 #[arg(id = "discovery.v5.bootstrap.lookup-countdown", long = "discovery.v5.bootstrap.lookup-countdown", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_COUNTDOWN",
576 default_value_t = DEFAULT_COUNT_BOOTSTRAP_LOOKUPS)]
577 pub discv5_bootstrap_lookup_countdown: u64,
578}
579
580impl DiscoveryArgs {
581 pub fn apply_to_builder<N>(
583 &self,
584 mut network_config_builder: NetworkConfigBuilder<N>,
585 rlpx_tcp_socket: SocketAddr,
586 boot_nodes: impl IntoIterator<Item = NodeRecord>,
587 ) -> NetworkConfigBuilder<N>
588 where
589 N: NetworkPrimitives,
590 {
591 if self.disable_discovery || self.disable_dns_discovery {
592 network_config_builder = network_config_builder.disable_dns_discovery();
593 }
594
595 if self.disable_discovery || self.disable_discv4_discovery {
596 network_config_builder = network_config_builder.disable_discv4_discovery();
597 }
598
599 if self.disable_nat {
600 network_config_builder = network_config_builder.disable_nat();
602 }
603
604 if self.should_enable_discv5() {
605 network_config_builder = network_config_builder
606 .discovery_v5(self.discovery_v5_builder(rlpx_tcp_socket, boot_nodes));
607 }
608
609 network_config_builder
610 }
611
612 pub fn discovery_v5_builder(
614 &self,
615 rlpx_tcp_socket: SocketAddr,
616 boot_nodes: impl IntoIterator<Item = NodeRecord>,
617 ) -> reth_discv5::ConfigBuilder {
618 let Self {
619 discv5_addr,
620 discv5_addr_ipv6,
621 discv5_port,
622 discv5_port_ipv6,
623 discv5_lookup_interval,
624 discv5_bootstrap_lookup_interval,
625 discv5_bootstrap_lookup_countdown,
626 ..
627 } = self;
628
629 let discv5_addr_ipv4 = discv5_addr.or(match rlpx_tcp_socket {
631 SocketAddr::V4(addr) => Some(*addr.ip()),
632 SocketAddr::V6(_) => None,
633 });
634 let discv5_addr_ipv6 = discv5_addr_ipv6.or(match rlpx_tcp_socket {
635 SocketAddr::V4(_) => None,
636 SocketAddr::V6(addr) => Some(*addr.ip()),
637 });
638
639 reth_discv5::Config::builder(rlpx_tcp_socket)
640 .discv5_config(
641 reth_discv5::discv5::ConfigBuilder::new(ListenConfig::from_two_sockets(
642 discv5_addr_ipv4.map(|addr| SocketAddrV4::new(addr, *discv5_port)),
643 discv5_addr_ipv6.map(|addr| SocketAddrV6::new(addr, *discv5_port_ipv6, 0, 0)),
644 ))
645 .build(),
646 )
647 .add_unsigned_boot_nodes(boot_nodes)
648 .lookup_interval(*discv5_lookup_interval)
649 .bootstrap_lookup_interval(*discv5_bootstrap_lookup_interval)
650 .bootstrap_lookup_countdown(*discv5_bootstrap_lookup_countdown)
651 }
652
653 const fn should_enable_discv5(&self) -> bool {
655 if self.disable_discovery {
656 return false;
657 }
658
659 self.enable_discv5_discovery ||
660 self.discv5_addr.is_some() ||
661 self.discv5_addr_ipv6.is_some()
662 }
663
664 pub const fn with_unused_discovery_port(mut self) -> Self {
667 self.port = 0;
668 self
669 }
670
671 pub const fn with_discv5_port(mut self, port: u16) -> Self {
673 self.discv5_port = port;
674 self
675 }
676
677 pub fn adjust_instance_ports(&mut self, instance: u16) {
683 debug_assert_ne!(instance, 0, "instance must be non-zero");
684 self.port += instance - 1;
685 self.discv5_port += instance - 1;
686 self.discv5_port_ipv6 += instance - 1;
687 }
688}
689
690impl Default for DiscoveryArgs {
691 fn default() -> Self {
692 Self {
693 disable_discovery: false,
694 disable_dns_discovery: false,
695 disable_discv4_discovery: false,
696 enable_discv5_discovery: false,
697 disable_nat: false,
698 addr: DEFAULT_DISCOVERY_ADDR,
699 port: DEFAULT_DISCOVERY_PORT,
700 discv5_addr: None,
701 discv5_addr_ipv6: None,
702 discv5_port: DEFAULT_DISCOVERY_V5_PORT,
703 discv5_port_ipv6: DEFAULT_DISCOVERY_V5_PORT,
704 discv5_lookup_interval: DEFAULT_SECONDS_LOOKUP_INTERVAL,
705 discv5_bootstrap_lookup_interval: DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL,
706 discv5_bootstrap_lookup_countdown: DEFAULT_COUNT_BOOTSTRAP_LOOKUPS,
707 }
708 }
709}
710
711fn parse_block_num_hash(s: &str) -> Result<BlockNumHash, String> {
713 if let Some((num_str, hash_str)) = s.split_once('=') {
714 let number = num_str.parse().map_err(|_| format!("Invalid block number: {}", num_str))?;
715 let hash = B256::from_str(hash_str).map_err(|_| format!("Invalid hash: {}", hash_str))?;
716 Ok(BlockNumHash::new(number, hash))
717 } else {
718 let hash = B256::from_str(s).map_err(|_| format!("Invalid hash: {}", s))?;
720 Ok(BlockNumHash::new(0, hash))
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use clap::Parser;
728 use reth_chainspec::MAINNET;
729 use reth_config::Config;
730 use reth_network_peers::NodeRecord;
731 use secp256k1::SecretKey;
732 use std::{
733 fs,
734 time::{SystemTime, UNIX_EPOCH},
735 };
736
737 #[derive(Parser)]
739 struct CommandParser<T: Args> {
740 #[command(flatten)]
741 args: T,
742 }
743
744 #[test]
745 fn parse_nat_args() {
746 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "none"]).args;
747 assert_eq!(args.nat, NatResolver::None);
748
749 let args =
750 CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "extip:0.0.0.0"]).args;
751 assert_eq!(args.nat, NatResolver::ExternalIp("0.0.0.0".parse().unwrap()));
752 }
753
754 #[test]
755 fn parse_peer_args() {
756 let args =
757 CommandParser::<NetworkArgs>::parse_from(["reth", "--max-outbound-peers", "50"]).args;
758 assert_eq!(args.max_outbound_peers, Some(50));
759 assert_eq!(args.max_inbound_peers, None);
760
761 let args = CommandParser::<NetworkArgs>::parse_from([
762 "reth",
763 "--max-outbound-peers",
764 "75",
765 "--max-inbound-peers",
766 "15",
767 ])
768 .args;
769 assert_eq!(args.max_outbound_peers, Some(75));
770 assert_eq!(args.max_inbound_peers, Some(15));
771 }
772
773 #[test]
774 fn parse_trusted_peer_args() {
775 let args =
776 CommandParser::<NetworkArgs>::parse_from([
777 "reth",
778 "--trusted-peers",
779 "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303,enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303"
780 ])
781 .args;
782
783 assert_eq!(
784 args.trusted_peers,
785 vec![
786 "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303".parse().unwrap(),
787 "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303".parse().unwrap()
788 ]
789 );
790 }
791
792 #[test]
793 fn parse_retry_strategy_args() {
794 let tests = vec![0, 10];
795
796 for retries in tests {
797 let retries_str = retries.to_string();
798 let args = CommandParser::<NetworkArgs>::parse_from([
799 "reth",
800 "--dns-retries",
801 retries_str.as_str(),
802 ])
803 .args;
804
805 assert_eq!(args.dns_retries, retries);
806 }
807 }
808
809 #[test]
810 fn parse_disable_tx_gossip_args() {
811 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--disable-tx-gossip"]).args;
812 assert!(args.disable_tx_gossip);
813 }
814
815 #[test]
816 fn parse_max_peers_flag() {
817 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "90"]).args;
818
819 assert_eq!(args.max_peers, Some(90));
820 assert_eq!(args.max_outbound_peers, None);
821 assert_eq!(args.max_inbound_peers, None);
822 assert_eq!(args.resolved_max_outbound_peers(), Some(30));
823 assert_eq!(args.resolved_max_inbound_peers(), Some(60));
824 }
825
826 #[test]
827 fn max_peers_conflicts_with_outbound() {
828 let result = CommandParser::<NetworkArgs>::try_parse_from([
829 "reth",
830 "--max-peers",
831 "90",
832 "--max-outbound-peers",
833 "50",
834 ]);
835 assert!(
836 result.is_err(),
837 "Should fail when both --max-peers and --max-outbound-peers are used"
838 );
839 }
840
841 #[test]
842 fn max_peers_conflicts_with_inbound() {
843 let result = CommandParser::<NetworkArgs>::try_parse_from([
844 "reth",
845 "--max-peers",
846 "90",
847 "--max-inbound-peers",
848 "30",
849 ]);
850 assert!(
851 result.is_err(),
852 "Should fail when both --max-peers and --max-inbound-peers are used"
853 );
854 }
855
856 #[test]
857 fn max_peers_split_calculation() {
858 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "90"]).args;
859
860 assert_eq!(args.max_peers, Some(90));
861 assert_eq!(args.resolved_max_outbound_peers(), Some(30));
862 assert_eq!(args.resolved_max_inbound_peers(), Some(60));
863 }
864
865 #[test]
866 fn max_peers_small_values() {
867 let args1 = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "1"]).args;
868 assert_eq!(args1.resolved_max_outbound_peers(), Some(1));
869 assert_eq!(args1.resolved_max_inbound_peers(), Some(0));
870
871 let args2 = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "2"]).args;
872 assert_eq!(args2.resolved_max_outbound_peers(), Some(1));
873 assert_eq!(args2.resolved_max_inbound_peers(), Some(1));
874
875 let args3 = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "3"]).args;
876 assert_eq!(args3.resolved_max_outbound_peers(), Some(1));
877 assert_eq!(args3.resolved_max_inbound_peers(), Some(2));
878 }
879
880 #[test]
881 fn resolved_peers_without_max_peers() {
882 let args = CommandParser::<NetworkArgs>::parse_from([
883 "reth",
884 "--max-outbound-peers",
885 "75",
886 "--max-inbound-peers",
887 "15",
888 ])
889 .args;
890
891 assert_eq!(args.max_peers, None);
892 assert_eq!(args.resolved_max_outbound_peers(), Some(75));
893 assert_eq!(args.resolved_max_inbound_peers(), Some(15));
894 }
895
896 #[test]
897 fn resolved_peers_with_defaults() {
898 let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
899
900 assert_eq!(args.max_peers, None);
901 assert_eq!(args.resolved_max_outbound_peers(), None);
902 assert_eq!(args.resolved_max_inbound_peers(), None);
903 }
904
905 #[test]
906 fn network_args_default_sanity_test() {
907 let default_args = NetworkArgs::default();
908 let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
909
910 assert_eq!(args, default_args);
911 }
912
913 #[test]
914 fn parse_required_block_hashes() {
915 let args = CommandParser::<NetworkArgs>::parse_from([
916 "reth",
917 "--required-block-hashes",
918 "0x1111111111111111111111111111111111111111111111111111111111111111,23115201=0x2222222222222222222222222222222222222222222222222222222222222222",
919 ])
920 .args;
921
922 assert_eq!(args.required_block_hashes.len(), 2);
923 assert_eq!(args.required_block_hashes[0].number, 0);
925 assert_eq!(
926 args.required_block_hashes[0].hash.to_string(),
927 "0x1111111111111111111111111111111111111111111111111111111111111111"
928 );
929 assert_eq!(args.required_block_hashes[1].number, 23115201);
931 assert_eq!(
932 args.required_block_hashes[1].hash.to_string(),
933 "0x2222222222222222222222222222222222222222222222222222222222222222"
934 );
935 }
936
937 #[test]
938 fn parse_empty_required_block_hashes() {
939 let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
940 assert!(args.required_block_hashes.is_empty());
941 }
942
943 #[test]
944 fn test_parse_block_num_hash() {
945 let result = parse_block_num_hash(
947 "0x1111111111111111111111111111111111111111111111111111111111111111",
948 );
949 assert!(result.is_ok());
950 assert_eq!(result.unwrap().number, 0);
951
952 let result = parse_block_num_hash(
954 "23115201=0x2222222222222222222222222222222222222222222222222222222222222222",
955 );
956 assert!(result.is_ok());
957 assert_eq!(result.unwrap().number, 23115201);
958
959 assert!(parse_block_num_hash("invalid").is_err());
961 assert!(parse_block_num_hash(
962 "abc=0x1111111111111111111111111111111111111111111111111111111111111111"
963 )
964 .is_err());
965 }
966
967 #[test]
968 fn parse_p2p_secret_key_hex() {
969 let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
970 let args =
971 CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
972
973 let expected: B256 = hex.parse().unwrap();
974 assert_eq!(args.p2p_secret_key_hex, Some(expected));
975 assert_eq!(args.p2p_secret_key, None);
976 }
977
978 #[test]
979 fn parse_p2p_secret_key_hex_with_0x_prefix() {
980 let hex = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
981 let args =
982 CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
983
984 let expected: B256 = hex.parse().unwrap();
985 assert_eq!(args.p2p_secret_key_hex, Some(expected));
986 assert_eq!(args.p2p_secret_key, None);
987 }
988
989 #[test]
990 fn test_p2p_secret_key_and_hex_are_mutually_exclusive() {
991 let result = CommandParser::<NetworkArgs>::try_parse_from([
992 "reth",
993 "--p2p-secret-key",
994 "/path/to/key",
995 "--p2p-secret-key-hex",
996 "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f",
997 ]);
998
999 assert!(result.is_err());
1000 }
1001
1002 #[test]
1003 fn test_secret_key_method_with_hex() {
1004 let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
1005 let args =
1006 CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
1007
1008 let temp_dir = std::env::temp_dir();
1009 let default_path = temp_dir.join("default_key");
1010 let secret_key = args.secret_key(default_path).unwrap();
1011
1012 assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), hex);
1014 }
1015
1016 #[test]
1017 fn parse_netrestrict_single_network() {
1018 let args =
1019 CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "192.168.0.0/16"])
1020 .args;
1021
1022 assert_eq!(args.netrestrict, Some("192.168.0.0/16".to_string()));
1023
1024 let ip_filter = args.ip_filter().unwrap();
1025 assert!(ip_filter.has_restrictions());
1026 assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
1027 assert!(!ip_filter.is_allowed(&"10.0.0.1".parse().unwrap()));
1028 }
1029
1030 #[test]
1031 fn parse_netrestrict_multiple_networks() {
1032 let args = CommandParser::<NetworkArgs>::parse_from([
1033 "reth",
1034 "--netrestrict",
1035 "192.168.0.0/16,10.0.0.0/8",
1036 ])
1037 .args;
1038
1039 assert_eq!(args.netrestrict, Some("192.168.0.0/16,10.0.0.0/8".to_string()));
1040
1041 let ip_filter = args.ip_filter().unwrap();
1042 assert!(ip_filter.has_restrictions());
1043 assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
1044 assert!(ip_filter.is_allowed(&"10.5.10.20".parse().unwrap()));
1045 assert!(!ip_filter.is_allowed(&"172.16.0.1".parse().unwrap()));
1046 }
1047
1048 #[test]
1049 fn parse_netrestrict_ipv6() {
1050 let args =
1051 CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "2001:db8::/32"])
1052 .args;
1053
1054 let ip_filter = args.ip_filter().unwrap();
1055 assert!(ip_filter.has_restrictions());
1056 assert!(ip_filter.is_allowed(&"2001:db8::1".parse().unwrap()));
1057 assert!(!ip_filter.is_allowed(&"2001:db9::1".parse().unwrap()));
1058 }
1059
1060 #[test]
1061 fn netrestrict_not_set() {
1062 let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
1063 assert_eq!(args.netrestrict, None);
1064
1065 let ip_filter = args.ip_filter().unwrap();
1066 assert!(!ip_filter.has_restrictions());
1067 assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
1068 assert!(ip_filter.is_allowed(&"10.0.0.1".parse().unwrap()));
1069 }
1070
1071 #[test]
1072 fn netrestrict_invalid_cidr() {
1073 let args =
1074 CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "invalid-cidr"])
1075 .args;
1076
1077 assert!(args.ip_filter().is_err());
1078 }
1079
1080 #[test]
1081 fn network_config_preserves_basic_nodes_from_peers_file() {
1082 let enode = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
1083 let unique = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
1084
1085 let peers_file = std::env::temp_dir().join(format!("reth_peers_test_{}.json", unique));
1086 fs::write(&peers_file, format!("[\"{}\"]", enode)).expect("write peers file");
1087
1088 let args = NetworkArgs {
1090 peers_file: Some(peers_file.clone()),
1091 no_persist_peers: false,
1092 ..Default::default()
1093 };
1094
1095 let secret_key = SecretKey::from_byte_array(&[1u8; 32]).unwrap();
1097 let builder = args.network_config::<reth_network::EthNetworkPrimitives>(
1098 &Config::default(),
1099 MAINNET.clone(),
1100 secret_key,
1101 peers_file.clone(),
1102 Runtime::test(),
1103 );
1104
1105 let net_cfg = builder.build_with_noop_provider(MAINNET.clone());
1106
1107 let node: NodeRecord = enode.parse().unwrap();
1109 assert!(net_cfg.peers_config.persisted_peers.iter().any(|p| p.record == node));
1110
1111 let _ = fs::remove_file(&peers_file);
1113 }
1114}