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