Skip to main content

reth_node_core/args/
network.rs

1//! clap [Args](clap::Args) for network related arguments.
2
3use alloy_eips::BlockNumHash;
4use alloy_primitives::B256;
5use std::{
6    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
7    ops::Not,
8    path::PathBuf,
9    sync::OnceLock,
10};
11
12use crate::version::version_metadata;
13use clap::Args;
14use reth_chainspec::EthChainSpec;
15use reth_cli_util::{get_secret_key, load_secret_key::SecretKeyError};
16use reth_config::Config;
17use reth_discv4::{NodeRecord, DEFAULT_DISCOVERY_ADDR, DEFAULT_DISCOVERY_PORT};
18use reth_discv5::{
19    discv5::ListenConfig, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_PORT,
20    DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL,
21};
22use reth_net_banlist::IpFilter;
23use reth_net_nat::{NatResolver, DEFAULT_NET_IF_NAME};
24use reth_network::{
25    transactions::{
26        config::{TransactionIngressPolicy, TransactionPropagationKind},
27        constants::{
28            tx_fetcher::{
29                DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
30                DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
31            },
32            tx_manager::{
33                DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
34            },
35        },
36        TransactionFetcherConfig, TransactionPropagationMode, TransactionsManagerConfig,
37        DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
38        SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
39    },
40    HelloMessageWithProtocols, NetworkConfigBuilder, NetworkPrimitives,
41};
42use reth_network_peers::{mainnet_nodes, TrustedPeer};
43use reth_tasks::Runtime;
44use secp256k1::SecretKey;
45use std::str::FromStr;
46use tracing::error;
47
48/// Global static network defaults
49static NETWORK_DEFAULTS: OnceLock<DefaultNetworkArgs> = OnceLock::new();
50
51/// Default values for network CLI arguments that can be customized.
52///
53/// Global defaults can be set via [`DefaultNetworkArgs::try_init`].
54#[derive(Debug, Clone)]
55pub struct DefaultNetworkArgs {
56    /// Default DNS retries.
57    pub dns_retries: usize,
58    /// Default NAT resolver.
59    pub nat: NatResolver,
60    /// Default network listening address.
61    pub addr: IpAddr,
62    /// Default network listening port.
63    pub port: u16,
64    /// Default max concurrent `GetPooledTransactions` requests.
65    pub max_concurrent_tx_requests: u32,
66    /// Default max concurrent `GetPooledTransactions` requests per peer.
67    pub max_concurrent_tx_requests_per_peer: u8,
68    /// Default max number of seen transactions to remember per peer.
69    pub max_seen_tx_history: u32,
70    /// Default max number of transactions to import concurrently.
71    pub max_pending_pool_imports: usize,
72    /// Default max accumulated byte size of transactions to pack in one response.
73    pub soft_limit_byte_size_pooled_transactions_response: usize,
74    /// Default max accumulated byte size of transactions to request in one request.
75    pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
76    /// Default max capacity of cache of hashes for transactions pending fetch.
77    pub max_capacity_cache_txns_pending_fetch: u32,
78    /// Default transaction propagation policy.
79    pub tx_propagation_policy: TransactionPropagationKind,
80    /// Default transaction ingress policy.
81    pub tx_ingress_policy: TransactionIngressPolicy,
82    /// Default transaction propagation mode.
83    pub propagation_mode: TransactionPropagationMode,
84}
85
86impl DefaultNetworkArgs {
87    /// Initialize the global network defaults with this configuration.
88    pub fn try_init(self) -> Result<(), Self> {
89        NETWORK_DEFAULTS.set(self)
90    }
91
92    /// Get a reference to the global network defaults.
93    pub fn get_global() -> &'static Self {
94        NETWORK_DEFAULTS.get_or_init(Self::default)
95    }
96
97    /// Set the default DNS retries.
98    pub const fn with_dns_retries(mut self, v: usize) -> Self {
99        self.dns_retries = v;
100        self
101    }
102
103    /// Set the default NAT resolver.
104    pub fn with_nat(mut self, v: NatResolver) -> Self {
105        self.nat = v;
106        self
107    }
108
109    /// Set the default network listening address.
110    pub const fn with_addr(mut self, v: IpAddr) -> Self {
111        self.addr = v;
112        self
113    }
114
115    /// Set the default network listening port.
116    pub const fn with_port(mut self, v: u16) -> Self {
117        self.port = v;
118        self
119    }
120
121    /// Set the default max concurrent `GetPooledTransactions` requests.
122    pub const fn with_max_concurrent_tx_requests(mut self, v: u32) -> Self {
123        self.max_concurrent_tx_requests = v;
124        self
125    }
126
127    /// Set the default max concurrent `GetPooledTransactions` requests per peer.
128    pub const fn with_max_concurrent_tx_requests_per_peer(mut self, v: u8) -> Self {
129        self.max_concurrent_tx_requests_per_peer = v;
130        self
131    }
132
133    /// Set the default max number of seen transactions to remember per peer.
134    pub const fn with_max_seen_tx_history(mut self, v: u32) -> Self {
135        self.max_seen_tx_history = v;
136        self
137    }
138
139    /// Set the default max number of transactions to import concurrently.
140    pub const fn with_max_pending_pool_imports(mut self, v: usize) -> Self {
141        self.max_pending_pool_imports = v;
142        self
143    }
144
145    /// Set the default max accumulated byte size of transactions to pack in one response.
146    pub const fn with_soft_limit_byte_size_pooled_transactions_response(
147        mut self,
148        v: usize,
149    ) -> Self {
150        self.soft_limit_byte_size_pooled_transactions_response = v;
151        self
152    }
153
154    /// Set the default max accumulated byte size of transactions to request in one request.
155    pub const fn with_soft_limit_byte_size_pooled_transactions_response_on_pack_request(
156        mut self,
157        v: usize,
158    ) -> Self {
159        self.soft_limit_byte_size_pooled_transactions_response_on_pack_request = v;
160        self
161    }
162
163    /// Set the default max capacity of cache of hashes for transactions pending fetch.
164    pub const fn with_max_capacity_cache_txns_pending_fetch(mut self, v: u32) -> Self {
165        self.max_capacity_cache_txns_pending_fetch = v;
166        self
167    }
168
169    /// Set the default transaction propagation policy.
170    pub const fn with_tx_propagation_policy(mut self, v: TransactionPropagationKind) -> Self {
171        self.tx_propagation_policy = v;
172        self
173    }
174
175    /// Set the default transaction ingress policy.
176    pub const fn with_tx_ingress_policy(mut self, v: TransactionIngressPolicy) -> Self {
177        self.tx_ingress_policy = v;
178        self
179    }
180
181    /// Set the default transaction propagation mode.
182    pub const fn with_propagation_mode(mut self, v: TransactionPropagationMode) -> Self {
183        self.propagation_mode = v;
184        self
185    }
186}
187
188impl Default for DefaultNetworkArgs {
189    fn default() -> Self {
190        Self {
191            dns_retries: 0,
192            nat: NatResolver::Any,
193            addr: DEFAULT_DISCOVERY_ADDR,
194            port: DEFAULT_DISCOVERY_PORT,
195            max_concurrent_tx_requests: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
196            max_concurrent_tx_requests_per_peer: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
197            max_seen_tx_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
198            max_pending_pool_imports: DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS,
199            soft_limit_byte_size_pooled_transactions_response:
200                SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
201            soft_limit_byte_size_pooled_transactions_response_on_pack_request:
202                DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
203            max_capacity_cache_txns_pending_fetch: DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
204            tx_propagation_policy: TransactionPropagationKind::default(),
205            tx_ingress_policy: TransactionIngressPolicy::default(),
206            propagation_mode: TransactionPropagationMode::Sqrt,
207        }
208    }
209}
210
211/// Parameters for configuring the network more granularity via CLI
212#[derive(Debug, Clone, Args, PartialEq, Eq)]
213#[command(next_help_heading = "Networking")]
214pub struct NetworkArgs {
215    /// Arguments to setup discovery service.
216    #[command(flatten)]
217    pub discovery: DiscoveryArgs,
218
219    #[expect(clippy::doc_markdown)]
220    /// Comma separated enode URLs of trusted peers for P2P connections.
221    ///
222    /// --trusted-peers enode://abcd@192.168.0.1:30303
223    #[arg(long, value_delimiter = ',')]
224    pub trusted_peers: Vec<TrustedPeer>,
225
226    /// Connect to or accept from trusted peers only
227    #[arg(long)]
228    pub trusted_only: bool,
229
230    /// Comma separated enode URLs for P2P discovery bootstrap.
231    ///
232    /// Will fall back to a network-specific default if not specified.
233    #[arg(long, value_delimiter = ',')]
234    pub bootnodes: Option<Vec<TrustedPeer>>,
235
236    /// Amount of DNS resolution requests retries to perform when peering.
237    #[arg(long, default_value_t = DefaultNetworkArgs::get_global().dns_retries)]
238    pub dns_retries: usize,
239
240    /// The path to the known peers file. Connected peers are dumped to this file on nodes
241    /// shutdown, and read on startup. Cannot be used with `--no-persist-peers`.
242    #[arg(long, value_name = "FILE", verbatim_doc_comment, conflicts_with = "no_persist_peers")]
243    pub peers_file: Option<PathBuf>,
244
245    /// Custom node identity
246    #[arg(long, value_name = "IDENTITY", default_value = version_metadata().p2p_client_version.as_ref())]
247    pub identity: String,
248
249    /// Secret key to use for this node.
250    ///
251    /// This will also deterministically set the peer ID. If not specified, it will be set in the
252    /// data dir for the chain being used.
253    #[arg(long, value_name = "PATH", conflicts_with = "p2p_secret_key_hex")]
254    pub p2p_secret_key: Option<PathBuf>,
255
256    /// Hex encoded secret key to use for this node.
257    ///
258    /// This will also deterministically set the peer ID. Cannot be used together with
259    /// `--p2p-secret-key`.
260    #[arg(long, value_name = "HEX", conflicts_with = "p2p_secret_key")]
261    pub p2p_secret_key_hex: Option<B256>,
262
263    /// Do not persist peers.
264    #[arg(long, verbatim_doc_comment)]
265    pub no_persist_peers: bool,
266
267    /// NAT resolution method (any|none|upnp|publicip|extip:\<IP\>)
268    #[arg(long, default_value_t = DefaultNetworkArgs::get_global().nat.clone())]
269    pub nat: NatResolver,
270
271    /// Network listening address
272    #[arg(long = "addr", value_name = "ADDR", default_value_t = DefaultNetworkArgs::get_global().addr)]
273    pub addr: IpAddr,
274
275    /// Network listening port
276    #[arg(long = "port", value_name = "PORT", default_value_t = DefaultNetworkArgs::get_global().port)]
277    pub port: u16,
278
279    /// Maximum number of outbound peers. default: 100
280    #[arg(long)]
281    pub max_outbound_peers: Option<usize>,
282
283    /// Maximum number of inbound peers. default: 30
284    #[arg(long)]
285    pub max_inbound_peers: Option<usize>,
286
287    /// Maximum number of total peers (inbound + outbound).
288    ///
289    /// Splits peers using approximately 2:1 inbound:outbound ratio. Cannot be used together with
290    /// `--max-outbound-peers` or `--max-inbound-peers`.
291    #[arg(
292        long,
293        value_name = "COUNT",
294        conflicts_with = "max_outbound_peers",
295        conflicts_with = "max_inbound_peers"
296    )]
297    pub max_peers: Option<usize>,
298
299    /// Max concurrent `GetPooledTransactions` requests.
300    #[arg(long = "max-tx-reqs", value_name = "COUNT", default_value_t = DefaultNetworkArgs::get_global().max_concurrent_tx_requests, verbatim_doc_comment)]
301    pub max_concurrent_tx_requests: u32,
302
303    /// Max concurrent `GetPooledTransactions` requests per peer.
304    #[arg(long = "max-tx-reqs-peer", value_name = "COUNT", default_value_t = DefaultNetworkArgs::get_global().max_concurrent_tx_requests_per_peer, verbatim_doc_comment)]
305    pub max_concurrent_tx_requests_per_peer: u8,
306
307    /// Max number of seen transactions to remember per peer.
308    ///
309    /// Default is 320 transaction hashes.
310    #[arg(long = "max-seen-tx-history", value_name = "COUNT", default_value_t = DefaultNetworkArgs::get_global().max_seen_tx_history, verbatim_doc_comment)]
311    pub max_seen_tx_history: u32,
312
313    #[arg(long = "max-pending-imports", value_name = "COUNT", default_value_t = DefaultNetworkArgs::get_global().max_pending_pool_imports, verbatim_doc_comment)]
314    /// Max number of transactions to import concurrently.
315    pub max_pending_pool_imports: usize,
316
317    /// Experimental, for usage in research. Sets the max accumulated byte size of transactions
318    /// to pack in one response.
319    /// Spec'd at 2MiB.
320    #[arg(long = "pooled-tx-response-soft-limit", value_name = "BYTES", default_value_t = DefaultNetworkArgs::get_global().soft_limit_byte_size_pooled_transactions_response, verbatim_doc_comment)]
321    pub soft_limit_byte_size_pooled_transactions_response: usize,
322
323    /// Experimental, for usage in research. Sets the max accumulated byte size of transactions to
324    /// request in one request.
325    ///
326    /// Since `RLPx` protocol version 68, the byte size of a transaction is shared as metadata in a
327    /// transaction announcement (see `RLPx` specs). This allows a node to request a specific size
328    /// response.
329    ///
330    /// By default, nodes request only 128 KiB worth of transactions, but should a peer request
331    /// more, up to 2 MiB, a node will answer with more than 128 KiB.
332    ///
333    /// Default is 128 KiB.
334    #[arg(long = "pooled-tx-pack-soft-limit", value_name = "BYTES", default_value_t = DefaultNetworkArgs::get_global().soft_limit_byte_size_pooled_transactions_response_on_pack_request, verbatim_doc_comment)]
335    pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
336
337    /// Max capacity of cache of hashes for transactions pending fetch.
338    #[arg(long = "max-tx-pending-fetch", value_name = "COUNT", default_value_t = DefaultNetworkArgs::get_global().max_capacity_cache_txns_pending_fetch, verbatim_doc_comment)]
339    pub max_capacity_cache_txns_pending_fetch: u32,
340
341    /// Name of network interface used to communicate with peers.
342    ///
343    /// If flag is set, but no value is passed, the default interface for docker `eth0` is tried.
344    #[arg(long = "net-if.experimental", conflicts_with = "addr", value_name = "IF_NAME")]
345    pub net_if: Option<String>,
346
347    /// Transaction Propagation Policy
348    ///
349    /// The policy determines which peers transactions are gossiped to.
350    #[arg(long = "tx-propagation-policy", default_value_t = DefaultNetworkArgs::get_global().tx_propagation_policy)]
351    pub tx_propagation_policy: TransactionPropagationKind,
352
353    /// Transaction ingress policy
354    ///
355    /// Determines which peers' transactions are accepted over P2P.
356    #[arg(long = "tx-ingress-policy", default_value_t = DefaultNetworkArgs::get_global().tx_ingress_policy)]
357    pub tx_ingress_policy: TransactionIngressPolicy,
358
359    /// Disable transaction pool gossip
360    ///
361    /// Disables gossiping of transactions in the mempool to peers. This can be omitted for
362    /// personal nodes, though providers should always opt to enable this flag.
363    #[arg(long = "disable-tx-gossip")]
364    pub disable_tx_gossip: bool,
365
366    /// Sets the transaction propagation mode by determining how new pending transactions are
367    /// propagated to other peers in full.
368    ///
369    /// Examples: sqrt, all, max:10
370    #[arg(
371        long = "tx-propagation-mode",
372        default_value_t = DefaultNetworkArgs::get_global().propagation_mode,
373        help = "Transaction propagation mode (sqrt, all, max:<number>)"
374    )]
375    pub propagation_mode: TransactionPropagationMode,
376
377    /// Comma separated list of required block hashes or block number=hash pairs.
378    /// Peers that don't have these blocks will be filtered out.
379    /// Format: hash or `block_number=hash` (e.g., 23115201=0x1234...)
380    #[arg(long = "required-block-hashes", value_delimiter = ',', value_parser = parse_block_num_hash)]
381    pub required_block_hashes: Vec<BlockNumHash>,
382
383    /// Optional network ID to override the chain specification's network ID for P2P connections
384    #[arg(long)]
385    pub network_id: Option<u64>,
386
387    /// Restrict network communication to the given IP networks (CIDR masks).
388    ///
389    /// Comma separated list of CIDR network specifications.
390    /// Only peers with IP addresses within these ranges will be allowed to connect.
391    ///
392    /// Example: --netrestrict "192.168.0.0/16,10.0.0.0/8"
393    #[arg(long, value_name = "NETRESTRICT")]
394    pub netrestrict: Option<String>,
395
396    /// Enforce EIP-868 ENR fork ID validation for discovered peers.
397    ///
398    /// When enabled, peers discovered without a confirmed fork ID are not added to the peer set
399    /// until their fork ID is verified via EIP-868 ENR request. This filters out peers from other
400    /// networks that pollute the discovery table.
401    #[arg(long)]
402    pub enforce_enr_fork_id: bool,
403}
404
405impl NetworkArgs {
406    /// Returns the resolved IP address.
407    pub fn resolved_addr(&self) -> IpAddr {
408        if let Some(ref if_name) = self.net_if {
409            let if_name = if if_name.is_empty() { DEFAULT_NET_IF_NAME } else { if_name };
410            return match reth_net_nat::net_if::resolve_net_if_ip(if_name) {
411                Ok(addr) => addr,
412                Err(err) => {
413                    error!(target: "reth::cli",
414                        if_name,
415                        %err,
416                        "Failed to read network interface IP"
417                    );
418
419                    DEFAULT_DISCOVERY_ADDR
420                }
421            };
422        }
423
424        self.addr
425    }
426
427    /// Returns the resolved bootnodes if any are provided.
428    pub fn resolved_bootnodes(&self) -> Option<Vec<NodeRecord>> {
429        self.bootnodes.clone().map(|bootnodes| {
430            bootnodes.into_iter().filter_map(|node| node.resolve_blocking().ok()).collect()
431        })
432    }
433
434    /// Returns the max inbound peers (2:1 ratio).
435    pub fn resolved_max_inbound_peers(&self) -> Option<usize> {
436        if let Some(max_peers) = self.max_peers {
437            if max_peers == 0 {
438                Some(0)
439            } else {
440                let outbound = (max_peers / 3).max(1);
441                Some(max_peers.saturating_sub(outbound))
442            }
443        } else {
444            self.max_inbound_peers
445        }
446    }
447
448    /// Returns the max outbound peers (1:2 ratio).
449    pub fn resolved_max_outbound_peers(&self) -> Option<usize> {
450        if let Some(max_peers) = self.max_peers {
451            if max_peers == 0 {
452                Some(0)
453            } else {
454                Some((max_peers / 3).max(1))
455            }
456        } else {
457            self.max_outbound_peers
458        }
459    }
460
461    /// Configures and returns a `TransactionsManagerConfig` based on the current settings.
462    pub const fn transactions_manager_config(&self) -> TransactionsManagerConfig {
463        TransactionsManagerConfig {
464            transaction_fetcher_config: TransactionFetcherConfig::new(
465                self.max_concurrent_tx_requests,
466                self.max_concurrent_tx_requests_per_peer,
467                self.soft_limit_byte_size_pooled_transactions_response,
468                self.soft_limit_byte_size_pooled_transactions_response_on_pack_request,
469                self.max_capacity_cache_txns_pending_fetch,
470            ),
471            max_transactions_seen_by_peer_history: self.max_seen_tx_history,
472            propagation_mode: self.propagation_mode,
473            ingress_policy: self.tx_ingress_policy,
474        }
475    }
476
477    /// Build a [`NetworkConfigBuilder`] from a [`Config`] and a [`EthChainSpec`], in addition to
478    /// the values in this option struct.
479    ///
480    /// The `default_peers_file` will be used as the default location to store the persistent peers
481    /// file if `no_persist_peers` is false, and there is no provided `peers_file`.
482    ///
483    /// Configured Bootnodes are prioritized, if unset, the chain spec bootnodes are used
484    /// Priority order for bootnodes configuration:
485    /// 1. --bootnodes flag
486    /// 2. Network preset flags (e.g. --holesky)
487    /// 3. default to mainnet nodes
488    pub fn network_config<N: NetworkPrimitives>(
489        &self,
490        config: &Config,
491        chain_spec: impl EthChainSpec,
492        secret_key: SecretKey,
493        default_peers_file: PathBuf,
494        executor: Runtime,
495    ) -> NetworkConfigBuilder<N> {
496        let addr = self.resolved_addr();
497        let chain_bootnodes = self
498            .resolved_bootnodes()
499            .unwrap_or_else(|| chain_spec.bootnodes().unwrap_or_else(mainnet_nodes));
500        let peers_file = self.peers_file.clone().unwrap_or(default_peers_file);
501
502        // Configure peer connections
503        let ip_filter = self.ip_filter().unwrap_or_default();
504        let peers_config = config
505            .peers_config_with_basic_nodes_from_file(
506                self.persistent_peers_file(peers_file).as_deref(),
507            )
508            .with_max_inbound_opt(self.resolved_max_inbound_peers())
509            .with_max_outbound_opt(self.resolved_max_outbound_peers())
510            .with_ip_filter(ip_filter)
511            .with_enforce_enr_fork_id(self.enforce_enr_fork_id);
512
513        // Configure basic network stack
514        NetworkConfigBuilder::<N>::new(secret_key, executor)
515            .external_ip_resolver(self.nat.clone())
516            .sessions_config(
517                config.sessions.clone().with_upscaled_event_buffer(peers_config.max_peers()),
518            )
519            .peer_config(peers_config)
520            .boot_nodes(chain_bootnodes.clone())
521            .transactions_manager_config(self.transactions_manager_config())
522            // Configure node identity
523            .apply(|builder| {
524                let peer_id = builder.get_peer_id();
525                builder.hello_message(
526                    HelloMessageWithProtocols::builder(peer_id)
527                        .client_version(&self.identity)
528                        .build(),
529                )
530            })
531            // apply discovery settings
532            .apply(|builder| {
533                let rlpx_socket = (addr, self.port).into();
534                self.discovery.apply_to_builder(builder, rlpx_socket, chain_bootnodes)
535            })
536            .listener_addr(SocketAddr::new(
537                addr, // set discovery port based on instance number
538                self.port,
539            ))
540            .discovery_addr(SocketAddr::new(
541                self.discovery.addr,
542                // set discovery port based on instance number
543                self.discovery.port,
544            ))
545            .disable_tx_gossip(self.disable_tx_gossip)
546            .required_block_hashes(self.required_block_hashes.clone())
547            .network_id(self.network_id)
548    }
549
550    /// If `no_persist_peers` is false then this returns the path to the persistent peers file path.
551    pub fn persistent_peers_file(&self, peers_file: PathBuf) -> Option<PathBuf> {
552        self.no_persist_peers.not().then_some(peers_file)
553    }
554
555    /// Configures the [`DiscoveryArgs`].
556    pub const fn with_discovery(mut self, discovery: DiscoveryArgs) -> Self {
557        self.discovery = discovery;
558        self
559    }
560
561    /// Sets the p2p port to zero, to allow the OS to assign a random unused port when
562    /// the network components bind to a socket.
563    pub const fn with_unused_p2p_port(mut self) -> Self {
564        self.port = 0;
565        self
566    }
567
568    /// Sets the p2p and discovery ports to zero, allowing the OD to assign a random unused port
569    /// when network components bind to sockets.
570    pub const fn with_unused_ports(mut self) -> Self {
571        self = self.with_unused_p2p_port();
572        self.discovery = self.discovery.with_unused_discovery_port();
573        self
574    }
575
576    /// Configures the [`NatResolver`]
577    pub fn with_nat_resolver(mut self, nat: NatResolver) -> Self {
578        self.nat = nat;
579        self
580    }
581
582    /// Change networking port numbers based on the instance number, if provided.
583    /// Ports are updated to `previous_value + instance - 1`
584    ///
585    /// # Panics
586    /// Warning: if `instance` is zero in debug mode, this will panic.
587    pub fn adjust_instance_ports(&mut self, instance: Option<u16>) {
588        if let Some(instance) = instance {
589            debug_assert_ne!(instance, 0, "instance must be non-zero");
590            self.port += instance - 1;
591            self.discovery.adjust_instance_ports(instance);
592        }
593    }
594
595    /// Resolve all trusted peers at once
596    pub async fn resolve_trusted_peers(&self) -> Result<Vec<NodeRecord>, std::io::Error> {
597        futures::future::try_join_all(
598            self.trusted_peers.iter().map(|peer| async move { peer.resolve().await }),
599        )
600        .await
601    }
602
603    /// Load the p2p secret key from the provided options.
604    ///
605    /// If `p2p_secret_key_hex` is provided, it will be used directly.
606    /// If `p2p_secret_key` is provided, it will be loaded from the file.
607    /// If neither is provided, the `default_secret_key_path` will be used.
608    pub fn secret_key(
609        &self,
610        default_secret_key_path: PathBuf,
611    ) -> Result<SecretKey, SecretKeyError> {
612        if let Some(b256) = &self.p2p_secret_key_hex {
613            // Use the B256 value directly (already validated as 32 bytes)
614            SecretKey::from_slice(b256.as_slice()).map_err(SecretKeyError::SecretKeyDecodeError)
615        } else {
616            // Load from file (either provided path or default)
617            let secret_key_path = self.p2p_secret_key.clone().unwrap_or(default_secret_key_path);
618            get_secret_key(&secret_key_path)
619        }
620    }
621
622    /// Creates an IP filter from the netrestrict argument.
623    ///
624    /// Returns an error if the CIDR format is invalid.
625    pub fn ip_filter(&self) -> Result<IpFilter, ipnet::AddrParseError> {
626        if let Some(netrestrict) = &self.netrestrict {
627            IpFilter::from_cidr_string(netrestrict)
628        } else {
629            Ok(IpFilter::allow_all())
630        }
631    }
632}
633
634impl Default for NetworkArgs {
635    fn default() -> Self {
636        let DefaultNetworkArgs {
637            dns_retries,
638            nat,
639            addr,
640            port,
641            max_concurrent_tx_requests,
642            max_concurrent_tx_requests_per_peer,
643            max_seen_tx_history,
644            max_pending_pool_imports,
645            soft_limit_byte_size_pooled_transactions_response,
646            soft_limit_byte_size_pooled_transactions_response_on_pack_request,
647            max_capacity_cache_txns_pending_fetch,
648            tx_propagation_policy,
649            tx_ingress_policy,
650            propagation_mode,
651        } = DefaultNetworkArgs::get_global().clone();
652        Self {
653            discovery: DiscoveryArgs::default(),
654            trusted_peers: vec![],
655            trusted_only: false,
656            bootnodes: None,
657            dns_retries,
658            peers_file: None,
659            identity: version_metadata().p2p_client_version.to_string(),
660            p2p_secret_key: None,
661            p2p_secret_key_hex: None,
662            no_persist_peers: false,
663            nat,
664            addr,
665            port,
666            max_outbound_peers: None,
667            max_inbound_peers: None,
668            max_peers: None,
669            max_concurrent_tx_requests,
670            max_concurrent_tx_requests_per_peer,
671            soft_limit_byte_size_pooled_transactions_response,
672            soft_limit_byte_size_pooled_transactions_response_on_pack_request,
673            max_pending_pool_imports,
674            max_seen_tx_history,
675            max_capacity_cache_txns_pending_fetch,
676            net_if: None,
677            tx_propagation_policy,
678            tx_ingress_policy,
679            disable_tx_gossip: false,
680            propagation_mode,
681            required_block_hashes: vec![],
682            network_id: None,
683            netrestrict: None,
684            enforce_enr_fork_id: false,
685        }
686    }
687}
688
689/// Arguments to setup discovery
690#[derive(Debug, Clone, Args, PartialEq, Eq)]
691pub struct DiscoveryArgs {
692    /// Disable the discovery service.
693    #[arg(short, long, default_value_if("dev", "true", "true"))]
694    pub disable_discovery: bool,
695
696    /// Disable the DNS discovery.
697    #[arg(long, conflicts_with = "disable_discovery")]
698    pub disable_dns_discovery: bool,
699
700    /// Disable Discv4 discovery.
701    #[arg(long, conflicts_with = "disable_discovery")]
702    pub disable_discv4_discovery: bool,
703
704    /// Enable Discv5 discovery.
705    #[arg(long, conflicts_with = "disable_discovery")]
706    pub enable_discv5_discovery: bool,
707
708    /// Disable Nat discovery.
709    #[arg(long, conflicts_with = "disable_discovery")]
710    pub disable_nat: bool,
711
712    /// The UDP address to use for devp2p peer discovery version 4.
713    #[arg(id = "discovery.addr", long = "discovery.addr", value_name = "DISCOVERY_ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
714    pub addr: IpAddr,
715
716    /// The UDP port to use for devp2p peer discovery version 4.
717    #[arg(id = "discovery.port", long = "discovery.port", value_name = "DISCOVERY_PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
718    pub port: u16,
719
720    /// The UDP IPv4 address to use for devp2p peer discovery version 5. Overwritten by `RLPx`
721    /// address, if it's also IPv4.
722    #[arg(id = "discovery.v5.addr", long = "discovery.v5.addr", value_name = "DISCOVERY_V5_ADDR", default_value = None)]
723    pub discv5_addr: Option<Ipv4Addr>,
724
725    /// The UDP IPv6 address to use for devp2p peer discovery version 5. Overwritten by `RLPx`
726    /// address, if it's also IPv6.
727    #[arg(id = "discovery.v5.addr.ipv6", long = "discovery.v5.addr.ipv6", value_name = "DISCOVERY_V5_ADDR_IPV6", default_value = None)]
728    pub discv5_addr_ipv6: Option<Ipv6Addr>,
729
730    /// The UDP IPv4 port to use for devp2p peer discovery version 5. Not used unless `--addr` is
731    /// IPv4, or `--discovery.v5.addr` is set.
732    #[arg(id = "discovery.v5.port", long = "discovery.v5.port", value_name = "DISCOVERY_V5_PORT",
733    default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
734    pub discv5_port: u16,
735
736    /// The UDP IPv6 port to use for devp2p peer discovery version 5. Not used unless `--addr` is
737    /// IPv6, or `--discovery.addr.ipv6` is set.
738    #[arg(id = "discovery.v5.port.ipv6", long = "discovery.v5.port.ipv6", value_name = "DISCOVERY_V5_PORT_IPV6",
739    default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
740    pub discv5_port_ipv6: u16,
741
742    /// The interval in seconds at which to carry out periodic lookup queries, for the whole
743    /// run of the program.
744    #[arg(id = "discovery.v5.lookup-interval", long = "discovery.v5.lookup-interval", value_name = "DISCOVERY_V5_LOOKUP_INTERVAL", default_value_t = DEFAULT_SECONDS_LOOKUP_INTERVAL)]
745    pub discv5_lookup_interval: u64,
746
747    /// The interval in seconds at which to carry out boost lookup queries, for a fixed number of
748    /// times, at bootstrap.
749    #[arg(id = "discovery.v5.bootstrap.lookup-interval", long = "discovery.v5.bootstrap.lookup-interval", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_INTERVAL",
750        default_value_t = DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL)]
751    pub discv5_bootstrap_lookup_interval: u64,
752
753    /// The number of times to carry out boost lookup queries at bootstrap.
754    #[arg(id = "discovery.v5.bootstrap.lookup-countdown", long = "discovery.v5.bootstrap.lookup-countdown", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_COUNTDOWN",
755        default_value_t = DEFAULT_COUNT_BOOTSTRAP_LOOKUPS)]
756    pub discv5_bootstrap_lookup_countdown: u64,
757}
758
759impl DiscoveryArgs {
760    /// Apply the discovery settings to the given [`NetworkConfigBuilder`]
761    pub fn apply_to_builder<N>(
762        &self,
763        mut network_config_builder: NetworkConfigBuilder<N>,
764        rlpx_tcp_socket: SocketAddr,
765        boot_nodes: impl IntoIterator<Item = NodeRecord>,
766    ) -> NetworkConfigBuilder<N>
767    where
768        N: NetworkPrimitives,
769    {
770        if self.disable_discovery || self.disable_dns_discovery {
771            network_config_builder = network_config_builder.disable_dns_discovery();
772        }
773
774        if self.disable_discovery || self.disable_discv4_discovery {
775            network_config_builder = network_config_builder.disable_discv4_discovery();
776        }
777
778        if self.disable_nat {
779            // we only check for `disable-nat` here and not for disable discovery because nat:extip can be used without discovery: <https://github.com/paradigmxyz/reth/issues/14878>
780            network_config_builder = network_config_builder.disable_nat();
781        }
782
783        if self.should_enable_discv5() {
784            network_config_builder = network_config_builder
785                .discovery_v5(self.discovery_v5_builder(rlpx_tcp_socket, boot_nodes));
786        }
787
788        network_config_builder
789    }
790
791    /// Creates a [`reth_discv5::ConfigBuilder`] filling it with the values from this struct.
792    pub fn discovery_v5_builder(
793        &self,
794        rlpx_tcp_socket: SocketAddr,
795        boot_nodes: impl IntoIterator<Item = NodeRecord>,
796    ) -> reth_discv5::ConfigBuilder {
797        let Self {
798            discv5_addr,
799            discv5_addr_ipv6,
800            discv5_port,
801            discv5_port_ipv6,
802            discv5_lookup_interval,
803            discv5_bootstrap_lookup_interval,
804            discv5_bootstrap_lookup_countdown,
805            ..
806        } = self;
807
808        let has_discv5_addr_args = discv5_addr.is_some() || discv5_addr_ipv6.is_some();
809
810        // Use rlpx address if none given
811        let discv5_addr_ipv4 = discv5_addr.or(match rlpx_tcp_socket {
812            SocketAddr::V4(addr) => Some(*addr.ip()),
813            SocketAddr::V6(_) => None,
814        });
815        let discv5_addr_ipv6 = discv5_addr_ipv6.or(match rlpx_tcp_socket {
816            SocketAddr::V4(_) => None,
817            SocketAddr::V6(addr) => Some(*addr.ip()),
818        });
819
820        let mut discv5_config_builder =
821            reth_discv5::discv5::ConfigBuilder::new(ListenConfig::from_two_sockets(
822                discv5_addr_ipv4.map(|addr| SocketAddrV4::new(addr, *discv5_port)),
823                discv5_addr_ipv6.map(|addr| SocketAddrV6::new(addr, *discv5_port_ipv6, 0, 0)),
824            ));
825
826        if has_discv5_addr_args || self.disable_nat {
827            // disable native enr update if addresses manually set or nat disabled
828            discv5_config_builder.disable_enr_update();
829        }
830        reth_discv5::Config::builder(rlpx_tcp_socket)
831            .discv5_config(discv5_config_builder.build())
832            .add_unsigned_boot_nodes(boot_nodes)
833            .lookup_interval(*discv5_lookup_interval)
834            .bootstrap_lookup_interval(*discv5_bootstrap_lookup_interval)
835            .bootstrap_lookup_countdown(*discv5_bootstrap_lookup_countdown)
836    }
837
838    /// Returns true if discv5 discovery should be configured
839    const fn should_enable_discv5(&self) -> bool {
840        if self.disable_discovery {
841            return false;
842        }
843
844        self.enable_discv5_discovery ||
845            self.discv5_addr.is_some() ||
846            self.discv5_addr_ipv6.is_some()
847    }
848
849    /// Set the discovery port to zero, to allow the OS to assign a random unused port when
850    /// discovery binds to the socket.
851    pub const fn with_unused_discovery_port(mut self) -> Self {
852        self.port = 0;
853        self
854    }
855
856    /// Set the discovery V5 port
857    pub const fn with_discv5_port(mut self, port: u16) -> Self {
858        self.discv5_port = port;
859        self
860    }
861
862    /// Change networking port numbers based on the instance number.
863    /// Ports are updated to `previous_value + instance - 1`
864    ///
865    /// # Panics
866    /// Warning: if `instance` is zero in debug mode, this will panic.
867    pub fn adjust_instance_ports(&mut self, instance: u16) {
868        debug_assert_ne!(instance, 0, "instance must be non-zero");
869        self.port += instance - 1;
870        self.discv5_port += instance - 1;
871        self.discv5_port_ipv6 += instance - 1;
872    }
873}
874
875impl Default for DiscoveryArgs {
876    fn default() -> Self {
877        Self {
878            disable_discovery: false,
879            disable_dns_discovery: false,
880            disable_discv4_discovery: false,
881            enable_discv5_discovery: false,
882            disable_nat: false,
883            addr: DEFAULT_DISCOVERY_ADDR,
884            port: DEFAULT_DISCOVERY_PORT,
885            discv5_addr: None,
886            discv5_addr_ipv6: None,
887            discv5_port: DEFAULT_DISCOVERY_V5_PORT,
888            discv5_port_ipv6: DEFAULT_DISCOVERY_V5_PORT,
889            discv5_lookup_interval: DEFAULT_SECONDS_LOOKUP_INTERVAL,
890            discv5_bootstrap_lookup_interval: DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL,
891            discv5_bootstrap_lookup_countdown: DEFAULT_COUNT_BOOTSTRAP_LOOKUPS,
892        }
893    }
894}
895
896/// Parse a block number=hash pair or just a hash into `BlockNumHash`
897fn parse_block_num_hash(s: &str) -> Result<BlockNumHash, String> {
898    if let Some((num_str, hash_str)) = s.split_once('=') {
899        let number = num_str.parse().map_err(|_| format!("Invalid block number: {}", num_str))?;
900        let hash = B256::from_str(hash_str).map_err(|_| format!("Invalid hash: {}", hash_str))?;
901        Ok(BlockNumHash::new(number, hash))
902    } else {
903        // For backward compatibility, treat as hash-only with number 0
904        let hash = B256::from_str(s).map_err(|_| format!("Invalid hash: {}", s))?;
905        Ok(BlockNumHash::new(0, hash))
906    }
907}
908
909#[cfg(test)]
910mod tests {
911    use super::*;
912    use clap::Parser;
913    use reth_chainspec::MAINNET;
914    use reth_config::Config;
915    use reth_network_peers::NodeRecord;
916    use secp256k1::SecretKey;
917    use std::{
918        fs,
919        time::{SystemTime, UNIX_EPOCH},
920    };
921
922    /// A helper type to parse Args more easily
923    #[derive(Parser)]
924    struct CommandParser<T: Args> {
925        #[command(flatten)]
926        args: T,
927    }
928
929    #[test]
930    fn parse_nat_args() {
931        let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "none"]).args;
932        assert_eq!(args.nat, NatResolver::None);
933
934        let args =
935            CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "extip:0.0.0.0"]).args;
936        assert_eq!(args.nat, NatResolver::ExternalIp("0.0.0.0".parse().unwrap()));
937    }
938
939    #[test]
940    fn parse_peer_args() {
941        let args =
942            CommandParser::<NetworkArgs>::parse_from(["reth", "--max-outbound-peers", "50"]).args;
943        assert_eq!(args.max_outbound_peers, Some(50));
944        assert_eq!(args.max_inbound_peers, None);
945
946        let args = CommandParser::<NetworkArgs>::parse_from([
947            "reth",
948            "--max-outbound-peers",
949            "75",
950            "--max-inbound-peers",
951            "15",
952        ])
953        .args;
954        assert_eq!(args.max_outbound_peers, Some(75));
955        assert_eq!(args.max_inbound_peers, Some(15));
956    }
957
958    #[test]
959    fn parse_trusted_peer_args() {
960        let args =
961            CommandParser::<NetworkArgs>::parse_from([
962            "reth",
963            "--trusted-peers",
964            "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303,enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303"
965        ])
966        .args;
967
968        assert_eq!(
969            args.trusted_peers,
970            vec![
971            "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303".parse().unwrap(),
972            "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303".parse().unwrap()
973            ]
974        );
975    }
976
977    #[test]
978    fn parse_retry_strategy_args() {
979        let tests = vec![0, 10];
980
981        for retries in tests {
982            let retries_str = retries.to_string();
983            let args = CommandParser::<NetworkArgs>::parse_from([
984                "reth",
985                "--dns-retries",
986                retries_str.as_str(),
987            ])
988            .args;
989
990            assert_eq!(args.dns_retries, retries);
991        }
992    }
993
994    #[test]
995    fn parse_disable_tx_gossip_args() {
996        let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--disable-tx-gossip"]).args;
997        assert!(args.disable_tx_gossip);
998    }
999
1000    #[test]
1001    fn parse_max_peers_flag() {
1002        let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "90"]).args;
1003
1004        assert_eq!(args.max_peers, Some(90));
1005        assert_eq!(args.max_outbound_peers, None);
1006        assert_eq!(args.max_inbound_peers, None);
1007        assert_eq!(args.resolved_max_outbound_peers(), Some(30));
1008        assert_eq!(args.resolved_max_inbound_peers(), Some(60));
1009    }
1010
1011    #[test]
1012    fn max_peers_conflicts_with_outbound() {
1013        let result = CommandParser::<NetworkArgs>::try_parse_from([
1014            "reth",
1015            "--max-peers",
1016            "90",
1017            "--max-outbound-peers",
1018            "50",
1019        ]);
1020        assert!(
1021            result.is_err(),
1022            "Should fail when both --max-peers and --max-outbound-peers are used"
1023        );
1024    }
1025
1026    #[test]
1027    fn max_peers_conflicts_with_inbound() {
1028        let result = CommandParser::<NetworkArgs>::try_parse_from([
1029            "reth",
1030            "--max-peers",
1031            "90",
1032            "--max-inbound-peers",
1033            "30",
1034        ]);
1035        assert!(
1036            result.is_err(),
1037            "Should fail when both --max-peers and --max-inbound-peers are used"
1038        );
1039    }
1040
1041    #[test]
1042    fn max_peers_split_calculation() {
1043        let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "90"]).args;
1044
1045        assert_eq!(args.max_peers, Some(90));
1046        assert_eq!(args.resolved_max_outbound_peers(), Some(30));
1047        assert_eq!(args.resolved_max_inbound_peers(), Some(60));
1048    }
1049
1050    #[test]
1051    fn max_peers_small_values() {
1052        let args1 = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "1"]).args;
1053        assert_eq!(args1.resolved_max_outbound_peers(), Some(1));
1054        assert_eq!(args1.resolved_max_inbound_peers(), Some(0));
1055
1056        let args2 = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "2"]).args;
1057        assert_eq!(args2.resolved_max_outbound_peers(), Some(1));
1058        assert_eq!(args2.resolved_max_inbound_peers(), Some(1));
1059
1060        let args3 = CommandParser::<NetworkArgs>::parse_from(["reth", "--max-peers", "3"]).args;
1061        assert_eq!(args3.resolved_max_outbound_peers(), Some(1));
1062        assert_eq!(args3.resolved_max_inbound_peers(), Some(2));
1063    }
1064
1065    #[test]
1066    fn resolved_peers_without_max_peers() {
1067        let args = CommandParser::<NetworkArgs>::parse_from([
1068            "reth",
1069            "--max-outbound-peers",
1070            "75",
1071            "--max-inbound-peers",
1072            "15",
1073        ])
1074        .args;
1075
1076        assert_eq!(args.max_peers, None);
1077        assert_eq!(args.resolved_max_outbound_peers(), Some(75));
1078        assert_eq!(args.resolved_max_inbound_peers(), Some(15));
1079    }
1080
1081    #[test]
1082    fn resolved_peers_with_defaults() {
1083        let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
1084
1085        assert_eq!(args.max_peers, None);
1086        assert_eq!(args.resolved_max_outbound_peers(), None);
1087        assert_eq!(args.resolved_max_inbound_peers(), None);
1088    }
1089
1090    #[test]
1091    fn network_args_default_sanity_test() {
1092        let default_args = NetworkArgs::default();
1093        let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
1094
1095        assert_eq!(args, default_args);
1096    }
1097
1098    #[test]
1099    fn parse_required_block_hashes() {
1100        let args = CommandParser::<NetworkArgs>::parse_from([
1101            "reth",
1102            "--required-block-hashes",
1103            "0x1111111111111111111111111111111111111111111111111111111111111111,23115201=0x2222222222222222222222222222222222222222222222222222222222222222",
1104        ])
1105        .args;
1106
1107        assert_eq!(args.required_block_hashes.len(), 2);
1108        // First hash without block number (should default to 0)
1109        assert_eq!(args.required_block_hashes[0].number, 0);
1110        assert_eq!(
1111            args.required_block_hashes[0].hash.to_string(),
1112            "0x1111111111111111111111111111111111111111111111111111111111111111"
1113        );
1114        // Second with block number=hash format
1115        assert_eq!(args.required_block_hashes[1].number, 23115201);
1116        assert_eq!(
1117            args.required_block_hashes[1].hash.to_string(),
1118            "0x2222222222222222222222222222222222222222222222222222222222222222"
1119        );
1120    }
1121
1122    #[test]
1123    fn parse_empty_required_block_hashes() {
1124        let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
1125        assert!(args.required_block_hashes.is_empty());
1126    }
1127
1128    #[test]
1129    fn test_parse_block_num_hash() {
1130        // Test hash only format
1131        let result = parse_block_num_hash(
1132            "0x1111111111111111111111111111111111111111111111111111111111111111",
1133        );
1134        assert!(result.is_ok());
1135        assert_eq!(result.unwrap().number, 0);
1136
1137        // Test block_number=hash format
1138        let result = parse_block_num_hash(
1139            "23115201=0x2222222222222222222222222222222222222222222222222222222222222222",
1140        );
1141        assert!(result.is_ok());
1142        assert_eq!(result.unwrap().number, 23115201);
1143
1144        // Test invalid formats
1145        assert!(parse_block_num_hash("invalid").is_err());
1146        assert!(parse_block_num_hash(
1147            "abc=0x1111111111111111111111111111111111111111111111111111111111111111"
1148        )
1149        .is_err());
1150    }
1151
1152    #[test]
1153    fn parse_p2p_secret_key_hex() {
1154        let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
1155        let args =
1156            CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
1157
1158        let expected: B256 = hex.parse().unwrap();
1159        assert_eq!(args.p2p_secret_key_hex, Some(expected));
1160        assert_eq!(args.p2p_secret_key, None);
1161    }
1162
1163    #[test]
1164    fn parse_p2p_secret_key_hex_with_0x_prefix() {
1165        let hex = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
1166        let args =
1167            CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
1168
1169        let expected: B256 = hex.parse().unwrap();
1170        assert_eq!(args.p2p_secret_key_hex, Some(expected));
1171        assert_eq!(args.p2p_secret_key, None);
1172    }
1173
1174    #[test]
1175    fn test_p2p_secret_key_and_hex_are_mutually_exclusive() {
1176        let result = CommandParser::<NetworkArgs>::try_parse_from([
1177            "reth",
1178            "--p2p-secret-key",
1179            "/path/to/key",
1180            "--p2p-secret-key-hex",
1181            "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f",
1182        ]);
1183
1184        assert!(result.is_err());
1185    }
1186
1187    #[test]
1188    fn test_secret_key_method_with_hex() {
1189        let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
1190        let args =
1191            CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
1192
1193        let temp_dir = std::env::temp_dir();
1194        let default_path = temp_dir.join("default_key");
1195        let secret_key = args.secret_key(default_path).unwrap();
1196
1197        // Verify the secret key matches the hex input
1198        assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), hex);
1199    }
1200
1201    #[test]
1202    fn parse_netrestrict_single_network() {
1203        let args =
1204            CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "192.168.0.0/16"])
1205                .args;
1206
1207        assert_eq!(args.netrestrict, Some("192.168.0.0/16".to_string()));
1208
1209        let ip_filter = args.ip_filter().unwrap();
1210        assert!(ip_filter.has_restrictions());
1211        assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
1212        assert!(!ip_filter.is_allowed(&"10.0.0.1".parse().unwrap()));
1213    }
1214
1215    #[test]
1216    fn parse_netrestrict_multiple_networks() {
1217        let args = CommandParser::<NetworkArgs>::parse_from([
1218            "reth",
1219            "--netrestrict",
1220            "192.168.0.0/16,10.0.0.0/8",
1221        ])
1222        .args;
1223
1224        assert_eq!(args.netrestrict, Some("192.168.0.0/16,10.0.0.0/8".to_string()));
1225
1226        let ip_filter = args.ip_filter().unwrap();
1227        assert!(ip_filter.has_restrictions());
1228        assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
1229        assert!(ip_filter.is_allowed(&"10.5.10.20".parse().unwrap()));
1230        assert!(!ip_filter.is_allowed(&"172.16.0.1".parse().unwrap()));
1231    }
1232
1233    #[test]
1234    fn parse_netrestrict_ipv6() {
1235        let args =
1236            CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "2001:db8::/32"])
1237                .args;
1238
1239        let ip_filter = args.ip_filter().unwrap();
1240        assert!(ip_filter.has_restrictions());
1241        assert!(ip_filter.is_allowed(&"2001:db8::1".parse().unwrap()));
1242        assert!(!ip_filter.is_allowed(&"2001:db9::1".parse().unwrap()));
1243    }
1244
1245    #[test]
1246    fn netrestrict_not_set() {
1247        let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
1248        assert_eq!(args.netrestrict, None);
1249
1250        let ip_filter = args.ip_filter().unwrap();
1251        assert!(!ip_filter.has_restrictions());
1252        assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
1253        assert!(ip_filter.is_allowed(&"10.0.0.1".parse().unwrap()));
1254    }
1255
1256    #[test]
1257    fn netrestrict_invalid_cidr() {
1258        let args =
1259            CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "invalid-cidr"])
1260                .args;
1261
1262        assert!(args.ip_filter().is_err());
1263    }
1264
1265    #[test]
1266    fn network_config_preserves_basic_nodes_from_peers_file() {
1267        let enode = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
1268        let unique = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
1269
1270        let peers_file = std::env::temp_dir().join(format!("reth_peers_test_{}.json", unique));
1271        fs::write(&peers_file, format!("[\"{}\"]", enode)).expect("write peers file");
1272
1273        // Build NetworkArgs with peers_file set and no_persist_peers=false
1274        let args = NetworkArgs {
1275            peers_file: Some(peers_file.clone()),
1276            no_persist_peers: false,
1277            ..Default::default()
1278        };
1279
1280        // Build the network config using a deterministic secret key
1281        let secret_key = SecretKey::from_byte_array(&[1u8; 32]).unwrap();
1282        let builder = args.network_config::<reth_network::EthNetworkPrimitives>(
1283            &Config::default(),
1284            MAINNET.clone(),
1285            secret_key,
1286            peers_file.clone(),
1287            Runtime::test(),
1288        );
1289
1290        let net_cfg = builder.build_with_noop_provider(MAINNET.clone());
1291
1292        // Assert persisted_peers contains our node (legacy format is auto-converted)
1293        let node: NodeRecord = enode.parse().unwrap();
1294        assert!(net_cfg.peers_config.persisted_peers.iter().any(|p| p.record == node));
1295
1296        // Cleanup
1297        let _ = fs::remove_file(&peers_file);
1298    }
1299}