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