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, SessionsConfig,
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    /// Max concurrent `GetPooledTransactions` requests.
123    #[arg(long = "max-tx-reqs", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS, verbatim_doc_comment)]
124    pub max_concurrent_tx_requests: u32,
125
126    /// Max concurrent `GetPooledTransactions` requests per peer.
127    #[arg(long = "max-tx-reqs-peer", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER, verbatim_doc_comment)]
128    pub max_concurrent_tx_requests_per_peer: u8,
129
130    /// Max number of seen transactions to remember per peer.
131    ///
132    /// Default is 320 transaction hashes.
133    #[arg(long = "max-seen-tx-history", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER, verbatim_doc_comment)]
134    pub max_seen_tx_history: u32,
135
136    #[arg(long = "max-pending-imports", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, verbatim_doc_comment)]
137    /// Max number of transactions to import concurrently.
138    pub max_pending_pool_imports: usize,
139
140    /// Experimental, for usage in research. Sets the max accumulated byte size of transactions
141    /// to pack in one response.
142    /// Spec'd at 2MiB.
143    #[arg(long = "pooled-tx-response-soft-limit", value_name = "BYTES", default_value_t = SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE, verbatim_doc_comment)]
144    pub soft_limit_byte_size_pooled_transactions_response: usize,
145
146    /// Experimental, for usage in research. Sets the max accumulated byte size of transactions to
147    /// request in one request.
148    ///
149    /// Since `RLPx` protocol version 68, the byte size of a transaction is shared as metadata in a
150    /// transaction announcement (see `RLPx` specs). This allows a node to request a specific size
151    /// response.
152    ///
153    /// By default, nodes request only 128 KiB worth of transactions, but should a peer request
154    /// more, up to 2 MiB, a node will answer with more than 128 KiB.
155    ///
156    /// Default is 128 KiB.
157    #[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)]
158    pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
159
160    /// Max capacity of cache of hashes for transactions pending fetch.
161    #[arg(long = "max-tx-pending-fetch", value_name = "COUNT", default_value_t = DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, verbatim_doc_comment)]
162    pub max_capacity_cache_txns_pending_fetch: u32,
163
164    /// Name of network interface used to communicate with peers.
165    ///
166    /// If flag is set, but no value is passed, the default interface for docker `eth0` is tried.
167    #[arg(long = "net-if.experimental", conflicts_with = "addr", value_name = "IF_NAME")]
168    pub net_if: Option<String>,
169
170    /// Transaction Propagation Policy
171    ///
172    /// The policy determines which peers transactions are gossiped to.
173    #[arg(long = "tx-propagation-policy", default_value_t = TransactionPropagationKind::All)]
174    pub tx_propagation_policy: TransactionPropagationKind,
175
176    /// Transaction ingress policy
177    ///
178    /// Determines which peers' transactions are accepted over P2P.
179    #[arg(long = "tx-ingress-policy", default_value_t = TransactionIngressPolicy::All)]
180    pub tx_ingress_policy: TransactionIngressPolicy,
181
182    /// Disable transaction pool gossip
183    ///
184    /// Disables gossiping of transactions in the mempool to peers. This can be omitted for
185    /// personal nodes, though providers should always opt to enable this flag.
186    #[arg(long = "disable-tx-gossip")]
187    pub disable_tx_gossip: bool,
188
189    /// Sets the transaction propagation mode by determining how new pending transactions are
190    /// propagated to other peers in full.
191    ///
192    /// Examples: sqrt, all, max:10
193    #[arg(
194        long = "tx-propagation-mode",
195        default_value = "sqrt",
196        help = "Transaction propagation mode (sqrt, all, max:<number>)"
197    )]
198    pub propagation_mode: TransactionPropagationMode,
199
200    /// Comma separated list of required block hashes or block number=hash pairs.
201    /// Peers that don't have these blocks will be filtered out.
202    /// Format: hash or `block_number=hash` (e.g., 23115201=0x1234...)
203    #[arg(long = "required-block-hashes", value_delimiter = ',', value_parser = parse_block_num_hash)]
204    pub required_block_hashes: Vec<BlockNumHash>,
205
206    /// Optional network ID to override the chain specification's network ID for P2P connections
207    #[arg(long)]
208    pub network_id: Option<u64>,
209
210    /// Restrict network communication to the given IP networks (CIDR masks).
211    ///
212    /// Comma separated list of CIDR network specifications.
213    /// Only peers with IP addresses within these ranges will be allowed to connect.
214    ///
215    /// Example: --netrestrict "192.168.0.0/16,10.0.0.0/8"
216    #[arg(long, value_name = "NETRESTRICT")]
217    pub netrestrict: Option<String>,
218}
219
220impl NetworkArgs {
221    /// Returns the resolved IP address.
222    pub fn resolved_addr(&self) -> IpAddr {
223        if let Some(ref if_name) = self.net_if {
224            let if_name = if if_name.is_empty() { DEFAULT_NET_IF_NAME } else { if_name };
225            return match reth_net_nat::net_if::resolve_net_if_ip(if_name) {
226                Ok(addr) => addr,
227                Err(err) => {
228                    error!(target: "reth::cli",
229                        if_name,
230                        %err,
231                        "Failed to read network interface IP"
232                    );
233
234                    DEFAULT_DISCOVERY_ADDR
235                }
236            };
237        }
238
239        self.addr
240    }
241
242    /// Returns the resolved bootnodes if any are provided.
243    pub fn resolved_bootnodes(&self) -> Option<Vec<NodeRecord>> {
244        self.bootnodes.clone().map(|bootnodes| {
245            bootnodes.into_iter().filter_map(|node| node.resolve_blocking().ok()).collect()
246        })
247    }
248    /// Configures and returns a `TransactionsManagerConfig` based on the current settings.
249    pub const fn transactions_manager_config(&self) -> TransactionsManagerConfig {
250        TransactionsManagerConfig {
251            transaction_fetcher_config: TransactionFetcherConfig::new(
252                self.max_concurrent_tx_requests,
253                self.max_concurrent_tx_requests_per_peer,
254                self.soft_limit_byte_size_pooled_transactions_response,
255                self.soft_limit_byte_size_pooled_transactions_response_on_pack_request,
256                self.max_capacity_cache_txns_pending_fetch,
257            ),
258            max_transactions_seen_by_peer_history: self.max_seen_tx_history,
259            propagation_mode: self.propagation_mode,
260            ingress_policy: self.tx_ingress_policy,
261        }
262    }
263
264    /// Build a [`NetworkConfigBuilder`] from a [`Config`] and a [`EthChainSpec`], in addition to
265    /// the values in this option struct.
266    ///
267    /// The `default_peers_file` will be used as the default location to store the persistent peers
268    /// file if `no_persist_peers` is false, and there is no provided `peers_file`.
269    ///
270    /// Configured Bootnodes are prioritized, if unset, the chain spec bootnodes are used
271    /// Priority order for bootnodes configuration:
272    /// 1. --bootnodes flag
273    /// 2. Network preset flags (e.g. --holesky)
274    /// 3. default to mainnet nodes
275    pub fn network_config<N: NetworkPrimitives>(
276        &self,
277        config: &Config,
278        chain_spec: impl EthChainSpec,
279        secret_key: SecretKey,
280        default_peers_file: PathBuf,
281    ) -> NetworkConfigBuilder<N> {
282        let addr = self.resolved_addr();
283        let chain_bootnodes = self
284            .resolved_bootnodes()
285            .unwrap_or_else(|| chain_spec.bootnodes().unwrap_or_else(mainnet_nodes));
286        let peers_file = self.peers_file.clone().unwrap_or(default_peers_file);
287
288        // Configure peer connections
289        let ip_filter = self.ip_filter().unwrap_or_default();
290        let peers_config = config
291            .peers
292            .clone()
293            .with_max_inbound_opt(self.max_inbound_peers)
294            .with_max_outbound_opt(self.max_outbound_peers)
295            .with_ip_filter(ip_filter);
296
297        // Configure basic network stack
298        NetworkConfigBuilder::<N>::new(secret_key)
299            .peer_config(config.peers_config_with_basic_nodes_from_file(
300                self.persistent_peers_file(peers_file).as_deref(),
301            ))
302            .external_ip_resolver(self.nat)
303            .sessions_config(
304                SessionsConfig::default().with_upscaled_event_buffer(peers_config.max_peers()),
305            )
306            .peer_config(peers_config)
307            .boot_nodes(chain_bootnodes.clone())
308            .transactions_manager_config(self.transactions_manager_config())
309            // Configure node identity
310            .apply(|builder| {
311                let peer_id = builder.get_peer_id();
312                builder.hello_message(
313                    HelloMessageWithProtocols::builder(peer_id)
314                        .client_version(&self.identity)
315                        .build(),
316                )
317            })
318            // apply discovery settings
319            .apply(|builder| {
320                let rlpx_socket = (addr, self.port).into();
321                self.discovery.apply_to_builder(builder, rlpx_socket, chain_bootnodes)
322            })
323            .listener_addr(SocketAddr::new(
324                addr, // set discovery port based on instance number
325                self.port,
326            ))
327            .discovery_addr(SocketAddr::new(
328                self.discovery.addr,
329                // set discovery port based on instance number
330                self.discovery.port,
331            ))
332            .disable_tx_gossip(self.disable_tx_gossip)
333            .required_block_hashes(self.required_block_hashes.clone())
334            .network_id(self.network_id)
335    }
336
337    /// If `no_persist_peers` is false then this returns the path to the persistent peers file path.
338    pub fn persistent_peers_file(&self, peers_file: PathBuf) -> Option<PathBuf> {
339        self.no_persist_peers.not().then_some(peers_file)
340    }
341
342    /// Configures the [`DiscoveryArgs`].
343    pub const fn with_discovery(mut self, discovery: DiscoveryArgs) -> Self {
344        self.discovery = discovery;
345        self
346    }
347
348    /// Sets the p2p port to zero, to allow the OS to assign a random unused port when
349    /// the network components bind to a socket.
350    pub const fn with_unused_p2p_port(mut self) -> Self {
351        self.port = 0;
352        self
353    }
354
355    /// Sets the p2p and discovery ports to zero, allowing the OD to assign a random unused port
356    /// when network components bind to sockets.
357    pub const fn with_unused_ports(mut self) -> Self {
358        self = self.with_unused_p2p_port();
359        self.discovery = self.discovery.with_unused_discovery_port();
360        self
361    }
362
363    /// Configures the [`NatResolver`]
364    pub const fn with_nat_resolver(mut self, nat: NatResolver) -> Self {
365        self.nat = nat;
366        self
367    }
368
369    /// Change networking port numbers based on the instance number, if provided.
370    /// Ports are updated to `previous_value + instance - 1`
371    ///
372    /// # Panics
373    /// Warning: if `instance` is zero in debug mode, this will panic.
374    pub fn adjust_instance_ports(&mut self, instance: Option<u16>) {
375        if let Some(instance) = instance {
376            debug_assert_ne!(instance, 0, "instance must be non-zero");
377            self.port += instance - 1;
378            self.discovery.adjust_instance_ports(instance);
379        }
380    }
381
382    /// Resolve all trusted peers at once
383    pub async fn resolve_trusted_peers(&self) -> Result<Vec<NodeRecord>, std::io::Error> {
384        futures::future::try_join_all(
385            self.trusted_peers.iter().map(|peer| async move { peer.resolve().await }),
386        )
387        .await
388    }
389
390    /// Load the p2p secret key from the provided options.
391    ///
392    /// If `p2p_secret_key_hex` is provided, it will be used directly.
393    /// If `p2p_secret_key` is provided, it will be loaded from the file.
394    /// If neither is provided, the `default_secret_key_path` will be used.
395    pub fn secret_key(
396        &self,
397        default_secret_key_path: PathBuf,
398    ) -> Result<SecretKey, SecretKeyError> {
399        if let Some(b256) = &self.p2p_secret_key_hex {
400            // Use the B256 value directly (already validated as 32 bytes)
401            SecretKey::from_slice(b256.as_slice()).map_err(SecretKeyError::SecretKeyDecodeError)
402        } else {
403            // Load from file (either provided path or default)
404            let secret_key_path = self.p2p_secret_key.clone().unwrap_or(default_secret_key_path);
405            get_secret_key(&secret_key_path)
406        }
407    }
408
409    /// Creates an IP filter from the netrestrict argument.
410    ///
411    /// Returns an error if the CIDR format is invalid.
412    pub fn ip_filter(&self) -> Result<IpFilter, ipnet::AddrParseError> {
413        if let Some(netrestrict) = &self.netrestrict {
414            IpFilter::from_cidr_string(netrestrict)
415        } else {
416            Ok(IpFilter::allow_all())
417        }
418    }
419}
420
421impl Default for NetworkArgs {
422    fn default() -> Self {
423        Self {
424            discovery: DiscoveryArgs::default(),
425            trusted_peers: vec![],
426            trusted_only: false,
427            bootnodes: None,
428            dns_retries: 0,
429            peers_file: None,
430            identity: version_metadata().p2p_client_version.to_string(),
431            p2p_secret_key: None,
432            p2p_secret_key_hex: None,
433            no_persist_peers: false,
434            nat: NatResolver::Any,
435            addr: DEFAULT_DISCOVERY_ADDR,
436            port: DEFAULT_DISCOVERY_PORT,
437            max_outbound_peers: None,
438            max_inbound_peers: None,
439            max_concurrent_tx_requests: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
440            max_concurrent_tx_requests_per_peer: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
441            soft_limit_byte_size_pooled_transactions_response:
442                SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
443            soft_limit_byte_size_pooled_transactions_response_on_pack_request: DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
444            max_pending_pool_imports: DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS,
445            max_seen_tx_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
446            max_capacity_cache_txns_pending_fetch: DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
447            net_if: None,
448            tx_propagation_policy: TransactionPropagationKind::default(),
449            tx_ingress_policy: TransactionIngressPolicy::default(),
450            disable_tx_gossip: false,
451            propagation_mode: TransactionPropagationMode::Sqrt,
452            required_block_hashes: vec![],
453            network_id: None,
454            netrestrict: None,
455        }
456    }
457}
458
459/// Arguments to setup discovery
460#[derive(Debug, Clone, Args, PartialEq, Eq)]
461pub struct DiscoveryArgs {
462    /// Disable the discovery service.
463    #[arg(short, long, default_value_if("dev", "true", "true"))]
464    pub disable_discovery: bool,
465
466    /// Disable the DNS discovery.
467    #[arg(long, conflicts_with = "disable_discovery")]
468    pub disable_dns_discovery: bool,
469
470    /// Disable Discv4 discovery.
471    #[arg(long, conflicts_with = "disable_discovery")]
472    pub disable_discv4_discovery: bool,
473
474    /// Enable Discv5 discovery.
475    #[arg(long, conflicts_with = "disable_discovery")]
476    pub enable_discv5_discovery: bool,
477
478    /// Disable Nat discovery.
479    #[arg(long, conflicts_with = "disable_discovery")]
480    pub disable_nat: bool,
481
482    /// The UDP address to use for devp2p peer discovery version 4.
483    #[arg(id = "discovery.addr", long = "discovery.addr", value_name = "DISCOVERY_ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
484    pub addr: IpAddr,
485
486    /// The UDP port to use for devp2p peer discovery version 4.
487    #[arg(id = "discovery.port", long = "discovery.port", value_name = "DISCOVERY_PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
488    pub port: u16,
489
490    /// The UDP IPv4 address to use for devp2p peer discovery version 5. Overwritten by `RLPx`
491    /// address, if it's also IPv4.
492    #[arg(id = "discovery.v5.addr", long = "discovery.v5.addr", value_name = "DISCOVERY_V5_ADDR", default_value = None)]
493    pub discv5_addr: Option<Ipv4Addr>,
494
495    /// The UDP IPv6 address to use for devp2p peer discovery version 5. Overwritten by `RLPx`
496    /// address, if it's also IPv6.
497    #[arg(id = "discovery.v5.addr.ipv6", long = "discovery.v5.addr.ipv6", value_name = "DISCOVERY_V5_ADDR_IPV6", default_value = None)]
498    pub discv5_addr_ipv6: Option<Ipv6Addr>,
499
500    /// The UDP IPv4 port to use for devp2p peer discovery version 5. Not used unless `--addr` is
501    /// IPv4, or `--discovery.v5.addr` is set.
502    #[arg(id = "discovery.v5.port", long = "discovery.v5.port", value_name = "DISCOVERY_V5_PORT",
503    default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
504    pub discv5_port: u16,
505
506    /// The UDP IPv6 port to use for devp2p peer discovery version 5. Not used unless `--addr` is
507    /// IPv6, or `--discovery.addr.ipv6` is set.
508    #[arg(id = "discovery.v5.port.ipv6", long = "discovery.v5.port.ipv6", value_name = "DISCOVERY_V5_PORT_IPV6",
509    default_value = None, default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
510    pub discv5_port_ipv6: u16,
511
512    /// The interval in seconds at which to carry out periodic lookup queries, for the whole
513    /// run of the program.
514    #[arg(id = "discovery.v5.lookup-interval", long = "discovery.v5.lookup-interval", value_name = "DISCOVERY_V5_LOOKUP_INTERVAL", default_value_t = DEFAULT_SECONDS_LOOKUP_INTERVAL)]
515    pub discv5_lookup_interval: u64,
516
517    /// The interval in seconds at which to carry out boost lookup queries, for a fixed number of
518    /// times, at bootstrap.
519    #[arg(id = "discovery.v5.bootstrap.lookup-interval", long = "discovery.v5.bootstrap.lookup-interval", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_INTERVAL",
520        default_value_t = DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL)]
521    pub discv5_bootstrap_lookup_interval: u64,
522
523    /// The number of times to carry out boost lookup queries at bootstrap.
524    #[arg(id = "discovery.v5.bootstrap.lookup-countdown", long = "discovery.v5.bootstrap.lookup-countdown", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_COUNTDOWN",
525        default_value_t = DEFAULT_COUNT_BOOTSTRAP_LOOKUPS)]
526    pub discv5_bootstrap_lookup_countdown: u64,
527}
528
529impl DiscoveryArgs {
530    /// Apply the discovery settings to the given [`NetworkConfigBuilder`]
531    pub fn apply_to_builder<N>(
532        &self,
533        mut network_config_builder: NetworkConfigBuilder<N>,
534        rlpx_tcp_socket: SocketAddr,
535        boot_nodes: impl IntoIterator<Item = NodeRecord>,
536    ) -> NetworkConfigBuilder<N>
537    where
538        N: NetworkPrimitives,
539    {
540        if self.disable_discovery || self.disable_dns_discovery {
541            network_config_builder = network_config_builder.disable_dns_discovery();
542        }
543
544        if self.disable_discovery || self.disable_discv4_discovery {
545            network_config_builder = network_config_builder.disable_discv4_discovery();
546        }
547
548        if self.disable_nat {
549            // 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>
550            network_config_builder = network_config_builder.disable_nat();
551        }
552
553        if self.should_enable_discv5() {
554            network_config_builder = network_config_builder
555                .discovery_v5(self.discovery_v5_builder(rlpx_tcp_socket, boot_nodes));
556        }
557
558        network_config_builder
559    }
560
561    /// Creates a [`reth_discv5::ConfigBuilder`] filling it with the values from this struct.
562    pub fn discovery_v5_builder(
563        &self,
564        rlpx_tcp_socket: SocketAddr,
565        boot_nodes: impl IntoIterator<Item = NodeRecord>,
566    ) -> reth_discv5::ConfigBuilder {
567        let Self {
568            discv5_addr,
569            discv5_addr_ipv6,
570            discv5_port,
571            discv5_port_ipv6,
572            discv5_lookup_interval,
573            discv5_bootstrap_lookup_interval,
574            discv5_bootstrap_lookup_countdown,
575            ..
576        } = self;
577
578        // Use rlpx address if none given
579        let discv5_addr_ipv4 = discv5_addr.or(match rlpx_tcp_socket {
580            SocketAddr::V4(addr) => Some(*addr.ip()),
581            SocketAddr::V6(_) => None,
582        });
583        let discv5_addr_ipv6 = discv5_addr_ipv6.or(match rlpx_tcp_socket {
584            SocketAddr::V4(_) => None,
585            SocketAddr::V6(addr) => Some(*addr.ip()),
586        });
587
588        reth_discv5::Config::builder(rlpx_tcp_socket)
589            .discv5_config(
590                reth_discv5::discv5::ConfigBuilder::new(ListenConfig::from_two_sockets(
591                    discv5_addr_ipv4.map(|addr| SocketAddrV4::new(addr, *discv5_port)),
592                    discv5_addr_ipv6.map(|addr| SocketAddrV6::new(addr, *discv5_port_ipv6, 0, 0)),
593                ))
594                .build(),
595            )
596            .add_unsigned_boot_nodes(boot_nodes)
597            .lookup_interval(*discv5_lookup_interval)
598            .bootstrap_lookup_interval(*discv5_bootstrap_lookup_interval)
599            .bootstrap_lookup_countdown(*discv5_bootstrap_lookup_countdown)
600    }
601
602    /// Returns true if discv5 discovery should be configured
603    const fn should_enable_discv5(&self) -> bool {
604        if self.disable_discovery {
605            return false;
606        }
607
608        self.enable_discv5_discovery ||
609            self.discv5_addr.is_some() ||
610            self.discv5_addr_ipv6.is_some()
611    }
612
613    /// Set the discovery port to zero, to allow the OS to assign a random unused port when
614    /// discovery binds to the socket.
615    pub const fn with_unused_discovery_port(mut self) -> Self {
616        self.port = 0;
617        self
618    }
619
620    /// Set the discovery V5 port
621    pub const fn with_discv5_port(mut self, port: u16) -> Self {
622        self.discv5_port = port;
623        self
624    }
625
626    /// Change networking port numbers based on the instance number.
627    /// Ports are updated to `previous_value + instance - 1`
628    ///
629    /// # Panics
630    /// Warning: if `instance` is zero in debug mode, this will panic.
631    pub fn adjust_instance_ports(&mut self, instance: u16) {
632        debug_assert_ne!(instance, 0, "instance must be non-zero");
633        self.port += instance - 1;
634        self.discv5_port += instance - 1;
635        self.discv5_port_ipv6 += instance - 1;
636    }
637}
638
639impl Default for DiscoveryArgs {
640    fn default() -> Self {
641        Self {
642            disable_discovery: false,
643            disable_dns_discovery: false,
644            disable_discv4_discovery: false,
645            enable_discv5_discovery: false,
646            disable_nat: false,
647            addr: DEFAULT_DISCOVERY_ADDR,
648            port: DEFAULT_DISCOVERY_PORT,
649            discv5_addr: None,
650            discv5_addr_ipv6: None,
651            discv5_port: DEFAULT_DISCOVERY_V5_PORT,
652            discv5_port_ipv6: DEFAULT_DISCOVERY_V5_PORT,
653            discv5_lookup_interval: DEFAULT_SECONDS_LOOKUP_INTERVAL,
654            discv5_bootstrap_lookup_interval: DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL,
655            discv5_bootstrap_lookup_countdown: DEFAULT_COUNT_BOOTSTRAP_LOOKUPS,
656        }
657    }
658}
659
660/// Parse a block number=hash pair or just a hash into `BlockNumHash`
661fn parse_block_num_hash(s: &str) -> Result<BlockNumHash, String> {
662    if let Some((num_str, hash_str)) = s.split_once('=') {
663        let number = num_str.parse().map_err(|_| format!("Invalid block number: {}", num_str))?;
664        let hash = B256::from_str(hash_str).map_err(|_| format!("Invalid hash: {}", hash_str))?;
665        Ok(BlockNumHash::new(number, hash))
666    } else {
667        // For backward compatibility, treat as hash-only with number 0
668        let hash = B256::from_str(s).map_err(|_| format!("Invalid hash: {}", s))?;
669        Ok(BlockNumHash::new(0, hash))
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use clap::Parser;
677    /// A helper type to parse Args more easily
678    #[derive(Parser)]
679    struct CommandParser<T: Args> {
680        #[command(flatten)]
681        args: T,
682    }
683
684    #[test]
685    fn parse_nat_args() {
686        let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "none"]).args;
687        assert_eq!(args.nat, NatResolver::None);
688
689        let args =
690            CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "extip:0.0.0.0"]).args;
691        assert_eq!(args.nat, NatResolver::ExternalIp("0.0.0.0".parse().unwrap()));
692    }
693
694    #[test]
695    fn parse_peer_args() {
696        let args =
697            CommandParser::<NetworkArgs>::parse_from(["reth", "--max-outbound-peers", "50"]).args;
698        assert_eq!(args.max_outbound_peers, Some(50));
699        assert_eq!(args.max_inbound_peers, None);
700
701        let args = CommandParser::<NetworkArgs>::parse_from([
702            "reth",
703            "--max-outbound-peers",
704            "75",
705            "--max-inbound-peers",
706            "15",
707        ])
708        .args;
709        assert_eq!(args.max_outbound_peers, Some(75));
710        assert_eq!(args.max_inbound_peers, Some(15));
711    }
712
713    #[test]
714    fn parse_trusted_peer_args() {
715        let args =
716            CommandParser::<NetworkArgs>::parse_from([
717            "reth",
718            "--trusted-peers",
719            "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303,enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303"
720        ])
721        .args;
722
723        assert_eq!(
724            args.trusted_peers,
725            vec![
726            "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303".parse().unwrap(),
727            "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303".parse().unwrap()
728            ]
729        );
730    }
731
732    #[test]
733    fn parse_retry_strategy_args() {
734        let tests = vec![0, 10];
735
736        for retries in tests {
737            let args = CommandParser::<NetworkArgs>::parse_from([
738                "reth",
739                "--dns-retries",
740                retries.to_string().as_str(),
741            ])
742            .args;
743
744            assert_eq!(args.dns_retries, retries);
745        }
746    }
747
748    #[test]
749    fn parse_disable_tx_gossip_args() {
750        let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--disable-tx-gossip"]).args;
751        assert!(args.disable_tx_gossip);
752    }
753
754    #[test]
755    fn network_args_default_sanity_test() {
756        let default_args = NetworkArgs::default();
757        let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
758
759        assert_eq!(args, default_args);
760    }
761
762    #[test]
763    fn parse_required_block_hashes() {
764        let args = CommandParser::<NetworkArgs>::parse_from([
765            "reth",
766            "--required-block-hashes",
767            "0x1111111111111111111111111111111111111111111111111111111111111111,23115201=0x2222222222222222222222222222222222222222222222222222222222222222",
768        ])
769        .args;
770
771        assert_eq!(args.required_block_hashes.len(), 2);
772        // First hash without block number (should default to 0)
773        assert_eq!(args.required_block_hashes[0].number, 0);
774        assert_eq!(
775            args.required_block_hashes[0].hash.to_string(),
776            "0x1111111111111111111111111111111111111111111111111111111111111111"
777        );
778        // Second with block number=hash format
779        assert_eq!(args.required_block_hashes[1].number, 23115201);
780        assert_eq!(
781            args.required_block_hashes[1].hash.to_string(),
782            "0x2222222222222222222222222222222222222222222222222222222222222222"
783        );
784    }
785
786    #[test]
787    fn parse_empty_required_block_hashes() {
788        let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
789        assert!(args.required_block_hashes.is_empty());
790    }
791
792    #[test]
793    fn test_parse_block_num_hash() {
794        // Test hash only format
795        let result = parse_block_num_hash(
796            "0x1111111111111111111111111111111111111111111111111111111111111111",
797        );
798        assert!(result.is_ok());
799        assert_eq!(result.unwrap().number, 0);
800
801        // Test block_number=hash format
802        let result = parse_block_num_hash(
803            "23115201=0x2222222222222222222222222222222222222222222222222222222222222222",
804        );
805        assert!(result.is_ok());
806        assert_eq!(result.unwrap().number, 23115201);
807
808        // Test invalid formats
809        assert!(parse_block_num_hash("invalid").is_err());
810        assert!(parse_block_num_hash(
811            "abc=0x1111111111111111111111111111111111111111111111111111111111111111"
812        )
813        .is_err());
814    }
815
816    #[test]
817    fn parse_p2p_secret_key_hex() {
818        let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
819        let args =
820            CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
821
822        let expected: B256 = hex.parse().unwrap();
823        assert_eq!(args.p2p_secret_key_hex, Some(expected));
824        assert_eq!(args.p2p_secret_key, None);
825    }
826
827    #[test]
828    fn parse_p2p_secret_key_hex_with_0x_prefix() {
829        let hex = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
830        let args =
831            CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
832
833        let expected: B256 = hex.parse().unwrap();
834        assert_eq!(args.p2p_secret_key_hex, Some(expected));
835        assert_eq!(args.p2p_secret_key, None);
836    }
837
838    #[test]
839    fn test_p2p_secret_key_and_hex_are_mutually_exclusive() {
840        let result = CommandParser::<NetworkArgs>::try_parse_from([
841            "reth",
842            "--p2p-secret-key",
843            "/path/to/key",
844            "--p2p-secret-key-hex",
845            "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f",
846        ]);
847
848        assert!(result.is_err());
849    }
850
851    #[test]
852    fn test_secret_key_method_with_hex() {
853        let hex = "4c0883a69102937d6231471b5dbb6204fe512961708279f8c5c58b3b9c4e8b8f";
854        let args =
855            CommandParser::<NetworkArgs>::parse_from(["reth", "--p2p-secret-key-hex", hex]).args;
856
857        let temp_dir = std::env::temp_dir();
858        let default_path = temp_dir.join("default_key");
859        let secret_key = args.secret_key(default_path).unwrap();
860
861        // Verify the secret key matches the hex input
862        assert_eq!(alloy_primitives::hex::encode(secret_key.secret_bytes()), hex);
863    }
864
865    #[test]
866    fn parse_netrestrict_single_network() {
867        let args =
868            CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "192.168.0.0/16"])
869                .args;
870
871        assert_eq!(args.netrestrict, Some("192.168.0.0/16".to_string()));
872
873        let ip_filter = args.ip_filter().unwrap();
874        assert!(ip_filter.has_restrictions());
875        assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
876        assert!(!ip_filter.is_allowed(&"10.0.0.1".parse().unwrap()));
877    }
878
879    #[test]
880    fn parse_netrestrict_multiple_networks() {
881        let args = CommandParser::<NetworkArgs>::parse_from([
882            "reth",
883            "--netrestrict",
884            "192.168.0.0/16,10.0.0.0/8",
885        ])
886        .args;
887
888        assert_eq!(args.netrestrict, Some("192.168.0.0/16,10.0.0.0/8".to_string()));
889
890        let ip_filter = args.ip_filter().unwrap();
891        assert!(ip_filter.has_restrictions());
892        assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
893        assert!(ip_filter.is_allowed(&"10.5.10.20".parse().unwrap()));
894        assert!(!ip_filter.is_allowed(&"172.16.0.1".parse().unwrap()));
895    }
896
897    #[test]
898    fn parse_netrestrict_ipv6() {
899        let args =
900            CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "2001:db8::/32"])
901                .args;
902
903        let ip_filter = args.ip_filter().unwrap();
904        assert!(ip_filter.has_restrictions());
905        assert!(ip_filter.is_allowed(&"2001:db8::1".parse().unwrap()));
906        assert!(!ip_filter.is_allowed(&"2001:db9::1".parse().unwrap()));
907    }
908
909    #[test]
910    fn netrestrict_not_set() {
911        let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
912        assert_eq!(args.netrestrict, None);
913
914        let ip_filter = args.ip_filter().unwrap();
915        assert!(!ip_filter.has_restrictions());
916        assert!(ip_filter.is_allowed(&"192.168.1.1".parse().unwrap()));
917        assert!(ip_filter.is_allowed(&"10.0.0.1".parse().unwrap()));
918    }
919
920    #[test]
921    fn netrestrict_invalid_cidr() {
922        let args =
923            CommandParser::<NetworkArgs>::parse_from(["reth", "--netrestrict", "invalid-cidr"])
924                .args;
925
926        assert!(args.ip_filter().is_err());
927    }
928}