Skip to main content

reth_node_core/args/
network.rs

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