1use alloy_primitives::B256;
4use std::{
5 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
6 ops::Not,
7 path::PathBuf,
8};
9
10use crate::version::version_metadata;
11use clap::Args;
12use reth_chainspec::EthChainSpec;
13use reth_config::Config;
14use reth_discv4::{NodeRecord, DEFAULT_DISCOVERY_ADDR, DEFAULT_DISCOVERY_PORT};
15use reth_discv5::{
16 discv5::ListenConfig, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_PORT,
17 DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL,
18};
19use reth_net_nat::{NatResolver, DEFAULT_NET_IF_NAME};
20use reth_network::{
21 transactions::{
22 config::TransactionPropagationKind,
23 constants::{
24 tx_fetcher::{
25 DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
26 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
27 },
28 tx_manager::{
29 DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
30 },
31 },
32 TransactionFetcherConfig, TransactionPropagationMode, TransactionsManagerConfig,
33 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
34 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
35 },
36 HelloMessageWithProtocols, NetworkConfigBuilder, NetworkPrimitives, SessionsConfig,
37};
38use reth_network_peers::{mainnet_nodes, TrustedPeer};
39use secp256k1::SecretKey;
40use tracing::error;
41
42#[derive(Debug, Clone, Args, PartialEq, Eq)]
44#[command(next_help_heading = "Networking")]
45pub struct NetworkArgs {
46 #[command(flatten)]
48 pub discovery: DiscoveryArgs,
49
50 #[expect(clippy::doc_markdown)]
51 #[arg(long, value_delimiter = ',')]
55 pub trusted_peers: Vec<TrustedPeer>,
56
57 #[arg(long)]
59 pub trusted_only: bool,
60
61 #[arg(long, value_delimiter = ',')]
65 pub bootnodes: Option<Vec<TrustedPeer>>,
66
67 #[arg(long, default_value_t = 0)]
69 pub dns_retries: usize,
70
71 #[arg(long, value_name = "FILE", verbatim_doc_comment, conflicts_with = "no_persist_peers")]
74 pub peers_file: Option<PathBuf>,
75
76 #[arg(long, value_name = "IDENTITY", default_value = version_metadata().p2p_client_version.as_ref())]
78 pub identity: String,
79
80 #[arg(long, value_name = "PATH")]
85 pub p2p_secret_key: Option<PathBuf>,
86
87 #[arg(long, verbatim_doc_comment)]
89 pub no_persist_peers: bool,
90
91 #[arg(long, default_value = "any")]
93 pub nat: NatResolver,
94
95 #[arg(long = "addr", value_name = "ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
97 pub addr: IpAddr,
98
99 #[arg(long = "port", value_name = "PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
101 pub port: u16,
102
103 #[arg(long)]
105 pub max_outbound_peers: Option<usize>,
106
107 #[arg(long)]
109 pub max_inbound_peers: Option<usize>,
110
111 #[arg(long = "max-tx-reqs", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS, verbatim_doc_comment)]
113 pub max_concurrent_tx_requests: u32,
114
115 #[arg(long = "max-tx-reqs-peer", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER, verbatim_doc_comment)]
117 pub max_concurrent_tx_requests_per_peer: u8,
118
119 #[arg(long = "max-seen-tx-history", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER, verbatim_doc_comment)]
123 pub max_seen_tx_history: u32,
124
125 #[arg(long = "max-pending-imports", value_name = "COUNT", default_value_t = DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, verbatim_doc_comment)]
126 pub max_pending_pool_imports: usize,
128
129 #[arg(long = "pooled-tx-response-soft-limit", value_name = "BYTES", default_value_t = SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE, verbatim_doc_comment)]
133 pub soft_limit_byte_size_pooled_transactions_response: usize,
134
135 #[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)]
147 pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
148
149 #[arg(long = "max-tx-pending-fetch", value_name = "COUNT", default_value_t = DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, verbatim_doc_comment)]
151 pub max_capacity_cache_txns_pending_fetch: u32,
152
153 #[arg(long = "net-if.experimental", conflicts_with = "addr", value_name = "IF_NAME")]
157 pub net_if: Option<String>,
158
159 #[arg(long = "tx-propagation-policy", default_value_t = TransactionPropagationKind::All)]
163 pub tx_propagation_policy: TransactionPropagationKind,
164
165 #[arg(long = "disable-tx-gossip")]
170 pub disable_tx_gossip: bool,
171
172 #[arg(
177 long = "tx-propagation-mode",
178 default_value = "sqrt",
179 help = "Transaction propagation mode (sqrt, all, max:<number>)"
180 )]
181 pub propagation_mode: TransactionPropagationMode,
182
183 #[arg(long = "required-block-hashes", value_delimiter = ',')]
186 pub required_block_hashes: Vec<B256>,
187}
188
189impl NetworkArgs {
190 pub fn resolved_addr(&self) -> IpAddr {
192 if let Some(ref if_name) = self.net_if {
193 let if_name = if if_name.is_empty() { DEFAULT_NET_IF_NAME } else { if_name };
194 return match reth_net_nat::net_if::resolve_net_if_ip(if_name) {
195 Ok(addr) => addr,
196 Err(err) => {
197 error!(target: "reth::cli",
198 if_name,
199 %err,
200 "Failed to read network interface IP"
201 );
202
203 DEFAULT_DISCOVERY_ADDR
204 }
205 };
206 }
207
208 self.addr
209 }
210
211 pub fn resolved_bootnodes(&self) -> Option<Vec<NodeRecord>> {
213 self.bootnodes.clone().map(|bootnodes| {
214 bootnodes.into_iter().filter_map(|node| node.resolve_blocking().ok()).collect()
215 })
216 }
217 pub const fn transactions_manager_config(&self) -> TransactionsManagerConfig {
219 TransactionsManagerConfig {
220 transaction_fetcher_config: TransactionFetcherConfig::new(
221 self.max_concurrent_tx_requests,
222 self.max_concurrent_tx_requests_per_peer,
223 self.soft_limit_byte_size_pooled_transactions_response,
224 self.soft_limit_byte_size_pooled_transactions_response_on_pack_request,
225 self.max_capacity_cache_txns_pending_fetch,
226 ),
227 max_transactions_seen_by_peer_history: self.max_seen_tx_history,
228 propagation_mode: self.propagation_mode,
229 }
230 }
231
232 pub fn network_config<N: NetworkPrimitives>(
244 &self,
245 config: &Config,
246 chain_spec: impl EthChainSpec,
247 secret_key: SecretKey,
248 default_peers_file: PathBuf,
249 ) -> NetworkConfigBuilder<N> {
250 let addr = self.resolved_addr();
251 let chain_bootnodes = self
252 .resolved_bootnodes()
253 .unwrap_or_else(|| chain_spec.bootnodes().unwrap_or_else(mainnet_nodes));
254 let peers_file = self.peers_file.clone().unwrap_or(default_peers_file);
255
256 let peers_config = config
258 .peers
259 .clone()
260 .with_max_inbound_opt(self.max_inbound_peers)
261 .with_max_outbound_opt(self.max_outbound_peers);
262
263 NetworkConfigBuilder::<N>::new(secret_key)
265 .peer_config(config.peers_config_with_basic_nodes_from_file(
266 self.persistent_peers_file(peers_file).as_deref(),
267 ))
268 .external_ip_resolver(self.nat)
269 .sessions_config(
270 SessionsConfig::default().with_upscaled_event_buffer(peers_config.max_peers()),
271 )
272 .peer_config(peers_config)
273 .boot_nodes(chain_bootnodes.clone())
274 .transactions_manager_config(self.transactions_manager_config())
275 .apply(|builder| {
277 let peer_id = builder.get_peer_id();
278 builder.hello_message(
279 HelloMessageWithProtocols::builder(peer_id)
280 .client_version(&self.identity)
281 .build(),
282 )
283 })
284 .apply(|builder| {
286 let rlpx_socket = (addr, self.port).into();
287 self.discovery.apply_to_builder(builder, rlpx_socket, chain_bootnodes)
288 })
289 .listener_addr(SocketAddr::new(
290 addr, self.port,
292 ))
293 .discovery_addr(SocketAddr::new(
294 self.discovery.addr,
295 self.discovery.port,
297 ))
298 .disable_tx_gossip(self.disable_tx_gossip)
299 .required_block_hashes(self.required_block_hashes.clone())
300 }
301
302 pub fn persistent_peers_file(&self, peers_file: PathBuf) -> Option<PathBuf> {
304 self.no_persist_peers.not().then_some(peers_file)
305 }
306
307 pub const fn with_unused_p2p_port(mut self) -> Self {
310 self.port = 0;
311 self
312 }
313
314 pub const fn with_unused_ports(mut self) -> Self {
317 self = self.with_unused_p2p_port();
318 self.discovery = self.discovery.with_unused_discovery_port();
319 self
320 }
321
322 pub fn adjust_instance_ports(&mut self, instance: Option<u16>) {
328 if let Some(instance) = instance {
329 debug_assert_ne!(instance, 0, "instance must be non-zero");
330 self.port += instance - 1;
331 self.discovery.adjust_instance_ports(instance);
332 }
333 }
334
335 pub async fn resolve_trusted_peers(&self) -> Result<Vec<NodeRecord>, std::io::Error> {
337 futures::future::try_join_all(
338 self.trusted_peers.iter().map(|peer| async move { peer.resolve().await }),
339 )
340 .await
341 }
342}
343
344impl Default for NetworkArgs {
345 fn default() -> Self {
346 Self {
347 discovery: DiscoveryArgs::default(),
348 trusted_peers: vec![],
349 trusted_only: false,
350 bootnodes: None,
351 dns_retries: 0,
352 peers_file: None,
353 identity: version_metadata().p2p_client_version.to_string(),
354 p2p_secret_key: None,
355 no_persist_peers: false,
356 nat: NatResolver::Any,
357 addr: DEFAULT_DISCOVERY_ADDR,
358 port: DEFAULT_DISCOVERY_PORT,
359 max_outbound_peers: None,
360 max_inbound_peers: None,
361 max_concurrent_tx_requests: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
362 max_concurrent_tx_requests_per_peer: DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
363 soft_limit_byte_size_pooled_transactions_response:
364 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
365 soft_limit_byte_size_pooled_transactions_response_on_pack_request: DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
366 max_pending_pool_imports: DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS,
367 max_seen_tx_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
368 max_capacity_cache_txns_pending_fetch: DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
369 net_if: None,
370 tx_propagation_policy: TransactionPropagationKind::default(),
371 disable_tx_gossip: false,
372 propagation_mode: TransactionPropagationMode::Sqrt,
373 required_block_hashes: vec![],
374 }
375 }
376}
377
378#[derive(Debug, Clone, Args, PartialEq, Eq)]
380pub struct DiscoveryArgs {
381 #[arg(short, long, default_value_if("dev", "true", "true"))]
383 pub disable_discovery: bool,
384
385 #[arg(long, conflicts_with = "disable_discovery")]
387 pub disable_dns_discovery: bool,
388
389 #[arg(long, conflicts_with = "disable_discovery")]
391 pub disable_discv4_discovery: bool,
392
393 #[arg(long, conflicts_with = "disable_discovery")]
395 pub enable_discv5_discovery: bool,
396
397 #[arg(long, conflicts_with = "disable_discovery")]
399 pub disable_nat: bool,
400
401 #[arg(id = "discovery.addr", long = "discovery.addr", value_name = "DISCOVERY_ADDR", default_value_t = DEFAULT_DISCOVERY_ADDR)]
403 pub addr: IpAddr,
404
405 #[arg(id = "discovery.port", long = "discovery.port", value_name = "DISCOVERY_PORT", default_value_t = DEFAULT_DISCOVERY_PORT)]
407 pub port: u16,
408
409 #[arg(id = "discovery.v5.addr", long = "discovery.v5.addr", value_name = "DISCOVERY_V5_ADDR", default_value = None)]
412 pub discv5_addr: Option<Ipv4Addr>,
413
414 #[arg(id = "discovery.v5.addr.ipv6", long = "discovery.v5.addr.ipv6", value_name = "DISCOVERY_V5_ADDR_IPV6", default_value = None)]
417 pub discv5_addr_ipv6: Option<Ipv6Addr>,
418
419 #[arg(id = "discovery.v5.port", long = "discovery.v5.port", value_name = "DISCOVERY_V5_PORT",
422 default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
423 pub discv5_port: u16,
424
425 #[arg(id = "discovery.v5.port.ipv6", long = "discovery.v5.port.ipv6", value_name = "DISCOVERY_V5_PORT_IPV6",
428 default_value = None, default_value_t = DEFAULT_DISCOVERY_V5_PORT)]
429 pub discv5_port_ipv6: u16,
430
431 #[arg(id = "discovery.v5.lookup-interval", long = "discovery.v5.lookup-interval", value_name = "DISCOVERY_V5_LOOKUP_INTERVAL", default_value_t = DEFAULT_SECONDS_LOOKUP_INTERVAL)]
434 pub discv5_lookup_interval: u64,
435
436 #[arg(id = "discovery.v5.bootstrap.lookup-interval", long = "discovery.v5.bootstrap.lookup-interval", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_INTERVAL",
439 default_value_t = DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL)]
440 pub discv5_bootstrap_lookup_interval: u64,
441
442 #[arg(id = "discovery.v5.bootstrap.lookup-countdown", long = "discovery.v5.bootstrap.lookup-countdown", value_name = "DISCOVERY_V5_BOOTSTRAP_LOOKUP_COUNTDOWN",
444 default_value_t = DEFAULT_COUNT_BOOTSTRAP_LOOKUPS)]
445 pub discv5_bootstrap_lookup_countdown: u64,
446}
447
448impl DiscoveryArgs {
449 pub fn apply_to_builder<N>(
451 &self,
452 mut network_config_builder: NetworkConfigBuilder<N>,
453 rlpx_tcp_socket: SocketAddr,
454 boot_nodes: impl IntoIterator<Item = NodeRecord>,
455 ) -> NetworkConfigBuilder<N>
456 where
457 N: NetworkPrimitives,
458 {
459 if self.disable_discovery || self.disable_dns_discovery {
460 network_config_builder = network_config_builder.disable_dns_discovery();
461 }
462
463 if self.disable_discovery || self.disable_discv4_discovery {
464 network_config_builder = network_config_builder.disable_discv4_discovery();
465 }
466
467 if self.disable_nat {
468 network_config_builder = network_config_builder.disable_nat();
470 }
471
472 if self.should_enable_discv5() {
473 network_config_builder = network_config_builder
474 .discovery_v5(self.discovery_v5_builder(rlpx_tcp_socket, boot_nodes));
475 }
476
477 network_config_builder
478 }
479
480 pub fn discovery_v5_builder(
482 &self,
483 rlpx_tcp_socket: SocketAddr,
484 boot_nodes: impl IntoIterator<Item = NodeRecord>,
485 ) -> reth_discv5::ConfigBuilder {
486 let Self {
487 discv5_addr,
488 discv5_addr_ipv6,
489 discv5_port,
490 discv5_port_ipv6,
491 discv5_lookup_interval,
492 discv5_bootstrap_lookup_interval,
493 discv5_bootstrap_lookup_countdown,
494 ..
495 } = self;
496
497 let discv5_addr_ipv4 = discv5_addr.or(match rlpx_tcp_socket {
499 SocketAddr::V4(addr) => Some(*addr.ip()),
500 SocketAddr::V6(_) => None,
501 });
502 let discv5_addr_ipv6 = discv5_addr_ipv6.or(match rlpx_tcp_socket {
503 SocketAddr::V4(_) => None,
504 SocketAddr::V6(addr) => Some(*addr.ip()),
505 });
506
507 reth_discv5::Config::builder(rlpx_tcp_socket)
508 .discv5_config(
509 reth_discv5::discv5::ConfigBuilder::new(ListenConfig::from_two_sockets(
510 discv5_addr_ipv4.map(|addr| SocketAddrV4::new(addr, *discv5_port)),
511 discv5_addr_ipv6.map(|addr| SocketAddrV6::new(addr, *discv5_port_ipv6, 0, 0)),
512 ))
513 .build(),
514 )
515 .add_unsigned_boot_nodes(boot_nodes)
516 .lookup_interval(*discv5_lookup_interval)
517 .bootstrap_lookup_interval(*discv5_bootstrap_lookup_interval)
518 .bootstrap_lookup_countdown(*discv5_bootstrap_lookup_countdown)
519 }
520
521 const fn should_enable_discv5(&self) -> bool {
523 if self.disable_discovery {
524 return false;
525 }
526
527 self.enable_discv5_discovery ||
528 self.discv5_addr.is_some() ||
529 self.discv5_addr_ipv6.is_some()
530 }
531
532 pub const fn with_unused_discovery_port(mut self) -> Self {
535 self.port = 0;
536 self
537 }
538
539 pub fn adjust_instance_ports(&mut self, instance: u16) {
545 debug_assert_ne!(instance, 0, "instance must be non-zero");
546 self.port += instance - 1;
547 self.discv5_port += instance - 1;
548 self.discv5_port_ipv6 += instance - 1;
549 }
550}
551
552impl Default for DiscoveryArgs {
553 fn default() -> Self {
554 Self {
555 disable_discovery: false,
556 disable_dns_discovery: false,
557 disable_discv4_discovery: false,
558 enable_discv5_discovery: false,
559 disable_nat: false,
560 addr: DEFAULT_DISCOVERY_ADDR,
561 port: DEFAULT_DISCOVERY_PORT,
562 discv5_addr: None,
563 discv5_addr_ipv6: None,
564 discv5_port: DEFAULT_DISCOVERY_V5_PORT,
565 discv5_port_ipv6: DEFAULT_DISCOVERY_V5_PORT,
566 discv5_lookup_interval: DEFAULT_SECONDS_LOOKUP_INTERVAL,
567 discv5_bootstrap_lookup_interval: DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL,
568 discv5_bootstrap_lookup_countdown: DEFAULT_COUNT_BOOTSTRAP_LOOKUPS,
569 }
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use clap::Parser;
577 #[derive(Parser)]
579 struct CommandParser<T: Args> {
580 #[command(flatten)]
581 args: T,
582 }
583
584 #[test]
585 fn parse_nat_args() {
586 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "none"]).args;
587 assert_eq!(args.nat, NatResolver::None);
588
589 let args =
590 CommandParser::<NetworkArgs>::parse_from(["reth", "--nat", "extip:0.0.0.0"]).args;
591 assert_eq!(args.nat, NatResolver::ExternalIp("0.0.0.0".parse().unwrap()));
592 }
593
594 #[test]
595 fn parse_peer_args() {
596 let args =
597 CommandParser::<NetworkArgs>::parse_from(["reth", "--max-outbound-peers", "50"]).args;
598 assert_eq!(args.max_outbound_peers, Some(50));
599 assert_eq!(args.max_inbound_peers, None);
600
601 let args = CommandParser::<NetworkArgs>::parse_from([
602 "reth",
603 "--max-outbound-peers",
604 "75",
605 "--max-inbound-peers",
606 "15",
607 ])
608 .args;
609 assert_eq!(args.max_outbound_peers, Some(75));
610 assert_eq!(args.max_inbound_peers, Some(15));
611 }
612
613 #[test]
614 fn parse_trusted_peer_args() {
615 let args =
616 CommandParser::<NetworkArgs>::parse_from([
617 "reth",
618 "--trusted-peers",
619 "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303,enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303"
620 ])
621 .args;
622
623 assert_eq!(
624 args.trusted_peers,
625 vec![
626 "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303".parse().unwrap(),
627 "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303".parse().unwrap()
628 ]
629 );
630 }
631
632 #[test]
633 fn parse_retry_strategy_args() {
634 let tests = vec![0, 10];
635
636 for retries in tests {
637 let args = CommandParser::<NetworkArgs>::parse_from([
638 "reth",
639 "--dns-retries",
640 retries.to_string().as_str(),
641 ])
642 .args;
643
644 assert_eq!(args.dns_retries, retries);
645 }
646 }
647
648 #[test]
649 fn parse_disable_tx_gossip_args() {
650 let args = CommandParser::<NetworkArgs>::parse_from(["reth", "--disable-tx-gossip"]).args;
651 assert!(args.disable_tx_gossip);
652 }
653
654 #[test]
655 fn network_args_default_sanity_test() {
656 let default_args = NetworkArgs::default();
657 let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
658
659 assert_eq!(args, default_args);
660 }
661
662 #[test]
663 fn parse_required_block_hashes() {
664 let args = CommandParser::<NetworkArgs>::parse_from([
665 "reth",
666 "--required-block-hashes",
667 "0x1111111111111111111111111111111111111111111111111111111111111111,0x2222222222222222222222222222222222222222222222222222222222222222",
668 ])
669 .args;
670
671 assert_eq!(args.required_block_hashes.len(), 2);
672 assert_eq!(
673 args.required_block_hashes[0].to_string(),
674 "0x1111111111111111111111111111111111111111111111111111111111111111"
675 );
676 assert_eq!(
677 args.required_block_hashes[1].to_string(),
678 "0x2222222222222222222222222222222222222222222222222222222222222222"
679 );
680 }
681
682 #[test]
683 fn parse_empty_required_block_hashes() {
684 let args = CommandParser::<NetworkArgs>::parse_from(["reth"]).args;
685 assert!(args.required_block_hashes.is_empty());
686 }
687}