Skip to main content

reth_node_core/args/
network.rs

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