1use crate::{
4 error::NetworkError,
5 import::{BlockImport, ProofOfStakeBlockImport},
6 transactions::TransactionsManagerConfig,
7 NetworkHandle, NetworkManager,
8};
9use alloy_eips::BlockNumHash;
10use reth_chainspec::{ChainSpecProvider, EthChainSpec, Hardforks};
11use reth_discv4::{Discv4Config, Discv4ConfigBuilder, NatResolver, DEFAULT_DISCOVERY_ADDRESS};
12use reth_discv5::NetworkStackId;
13use reth_dns_discovery::DnsDiscoveryConfig;
14use reth_eth_wire::{
15 handshake::{EthHandshake, EthRlpxHandshake},
16 EthNetworkPrimitives, HelloMessage, HelloMessageWithProtocols, NetworkPrimitives,
17 UnifiedStatus,
18};
19use reth_eth_wire_types::message::MAX_MESSAGE_SIZE;
20use reth_ethereum_forks::{ForkFilter, Head};
21use reth_network_peers::{mainnet_nodes, pk2id, sepolia_nodes, PeerId, TrustedPeer};
22use reth_network_types::{PeersConfig, SessionsConfig};
23use reth_storage_api::{
24 noop::NoopProvider, BalProvider, BlockNumReader, BlockReader, HeaderProvider,
25 StateProviderFactory, StateRangeProviderFactory,
26};
27use reth_tasks::Runtime;
28use secp256k1::SECP256K1;
29use std::{collections::HashSet, net::SocketAddr, sync::Arc};
30
31use crate::{
33 protocol::{IntoRlpxSubProtocol, RlpxSubProtocols},
34 transactions::TransactionPropagationMode,
35};
36pub use secp256k1::SecretKey;
37
38pub fn rng_secret_key() -> SecretKey {
40 SecretKey::new(&mut rand_08::thread_rng())
41}
42
43#[derive(Debug)]
45pub struct NetworkConfig<C, N: NetworkPrimitives = EthNetworkPrimitives> {
46 pub client: C,
51 pub secret_key: SecretKey,
53 pub boot_nodes: HashSet<TrustedPeer>,
55 pub dns_discovery_config: Option<DnsDiscoveryConfig>,
57 pub discovery_v4_addr: SocketAddr,
59 pub discovery_v4_config: Option<Discv4Config>,
61 pub discovery_v5_config: Option<reth_discv5::Config>,
63 pub listener_addr: SocketAddr,
65 pub peers_config: PeersConfig,
67 pub sessions_config: SessionsConfig,
69 pub chain_id: u64,
71 pub fork_filter: ForkFilter,
78 pub block_import: Box<dyn BlockImport<N::NewBlockPayload>>,
80 pub network_mode: NetworkMode,
82 pub executor: Runtime,
84 pub status: UnifiedStatus,
86 pub hello_message: HelloMessageWithProtocols,
88 pub extra_protocols: RlpxSubProtocols,
93 pub tx_gossip_disabled: bool,
95 pub transactions_manager_config: TransactionsManagerConfig,
97 pub nat: Option<NatResolver>,
99 pub handshake: Arc<dyn EthRlpxHandshake>,
104 pub eth_max_message_size: usize,
106 pub required_block_hashes: Vec<BlockNumHash>,
109}
110
111impl<N: NetworkPrimitives> NetworkConfig<(), N> {
114 pub fn builder(secret_key: SecretKey, executor: Runtime) -> NetworkConfigBuilder<N> {
116 NetworkConfigBuilder::new(secret_key, executor)
117 }
118
119 pub fn builder_with_rng_secret_key(executor: Runtime) -> NetworkConfigBuilder<N> {
121 NetworkConfigBuilder::with_rng_secret_key(executor)
122 }
123}
124
125impl<C, N: NetworkPrimitives> NetworkConfig<C, N> {
126 pub fn apply<F>(self, f: F) -> Self
128 where
129 F: FnOnce(Self) -> Self,
130 {
131 f(self)
132 }
133
134 pub fn set_discovery_v4(mut self, discovery_config: Discv4Config) -> Self {
136 self.discovery_v4_config = Some(discovery_config);
137 self
138 }
139
140 pub const fn set_listener_addr(mut self, listener_addr: SocketAddr) -> Self {
142 self.listener_addr = listener_addr;
143 self
144 }
145
146 pub const fn listener_addr(&self) -> &SocketAddr {
148 &self.listener_addr
149 }
150}
151
152impl<C, N> NetworkConfig<C, N>
153where
154 C: BlockNumReader + 'static,
155 N: NetworkPrimitives,
156{
157 pub async fn manager(self) -> Result<NetworkManager<N>, NetworkError> {
159 NetworkManager::new(self).await
160 }
161}
162
163impl<C, N> NetworkConfig<C, N>
164where
165 N: NetworkPrimitives,
166 C: BalProvider
167 + StateProviderFactory
168 + StateRangeProviderFactory
169 + BlockReader<Block = N::Block, Receipt = N::Receipt, Header = N::BlockHeader>
170 + HeaderProvider
171 + Clone
172 + Unpin
173 + 'static,
174{
175 pub async fn start_network(self) -> Result<NetworkHandle<N>, NetworkError> {
177 let client = self.client.clone();
178 let (handle, network, _txpool, eth) = NetworkManager::builder::<C>(self)
179 .await?
180 .request_handler::<C>(client)
181 .split_with_handle();
182
183 tokio::task::spawn(network);
184 tokio::task::spawn(eth);
185 Ok(handle)
186 }
187}
188
189#[derive(Debug)]
191pub struct NetworkConfigBuilder<N: NetworkPrimitives = EthNetworkPrimitives> {
192 secret_key: SecretKey,
194 dns_discovery_config: Option<DnsDiscoveryConfig>,
196 discovery_v4_builder: Option<Discv4ConfigBuilder>,
198 discovery_v5_builder: Option<reth_discv5::ConfigBuilder>,
200 boot_nodes: HashSet<TrustedPeer>,
202 discovery_addr: Option<SocketAddr>,
204 listener_addr: Option<SocketAddr>,
206 peers_config: Option<PeersConfig>,
208 sessions_config: Option<SessionsConfig>,
210 network_mode: NetworkMode,
212 executor: Runtime,
214 hello_message: Option<HelloMessageWithProtocols>,
216 extra_protocols: RlpxSubProtocols,
219 head: Option<Head>,
221 tx_gossip_disabled: bool,
223 block_import: Option<Box<dyn BlockImport<N::NewBlockPayload>>>,
225 transactions_manager_config: TransactionsManagerConfig,
227 nat: Option<NatResolver>,
229 handshake: Arc<dyn EthRlpxHandshake>,
232 eth_max_message_size: usize,
234 required_block_hashes: Vec<BlockNumHash>,
236 network_id: Option<u64>,
238 snap_enabled: bool,
240}
241
242impl NetworkConfigBuilder<EthNetworkPrimitives> {
243 pub fn eth(secret_key: SecretKey, executor: Runtime) -> Self {
245 Self::new(secret_key, executor)
246 }
247}
248
249#[expect(missing_docs)]
252impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
253 pub fn with_rng_secret_key(executor: Runtime) -> Self {
255 Self::new(rng_secret_key(), executor)
256 }
257
258 pub fn new(secret_key: SecretKey, executor: Runtime) -> Self {
260 Self {
261 secret_key,
262 dns_discovery_config: Some(Default::default()),
263 discovery_v4_builder: Some(Default::default()),
264 discovery_v5_builder: None,
265 boot_nodes: Default::default(),
266 discovery_addr: None,
267 listener_addr: None,
268 peers_config: None,
269 sessions_config: None,
270 network_mode: Default::default(),
271 executor,
272 hello_message: None,
273 extra_protocols: Default::default(),
274 head: None,
275 tx_gossip_disabled: false,
276 block_import: None,
277 transactions_manager_config: Default::default(),
278 nat: None,
279 handshake: Arc::new(EthHandshake::default()),
280 eth_max_message_size: MAX_MESSAGE_SIZE,
281 required_block_hashes: Vec::new(),
282 network_id: None,
283 snap_enabled: false,
284 }
285 }
286
287 pub fn apply<F>(self, f: F) -> Self
289 where
290 F: FnOnce(Self) -> Self,
291 {
292 f(self)
293 }
294
295 pub fn get_peer_id(&self) -> PeerId {
297 pk2id(&self.secret_key.public_key(SECP256K1))
298 }
299
300 pub const fn secret_key(&self) -> &SecretKey {
302 &self.secret_key
303 }
304
305 pub const fn network_mode(mut self, network_mode: NetworkMode) -> Self {
307 self.network_mode = network_mode;
308 self
309 }
310
311 pub const fn with_pow(self) -> Self {
319 self.network_mode(NetworkMode::Work)
320 }
321
322 pub const fn set_head(mut self, head: Head) -> Self {
328 self.head = Some(head);
329 self
330 }
331
332 pub fn hello_message(mut self, hello_message: HelloMessageWithProtocols) -> Self {
343 self.hello_message = Some(hello_message);
344 self
345 }
346
347 pub fn peer_config(mut self, config: PeersConfig) -> Self {
349 self.peers_config = Some(config);
350 self
351 }
352
353 pub fn with_task_executor(mut self, executor: Runtime) -> Self {
355 self.executor = executor;
356 self
357 }
358
359 pub const fn sessions_config(mut self, config: SessionsConfig) -> Self {
361 self.sessions_config = Some(config);
362 self
363 }
364
365 pub const fn transactions_manager_config(mut self, config: TransactionsManagerConfig) -> Self {
367 self.transactions_manager_config = config;
368 self
369 }
370
371 pub const fn transaction_propagation_mode(mut self, mode: TransactionPropagationMode) -> Self {
373 self.transactions_manager_config.propagation_mode = mode;
374 self
375 }
376
377 pub const fn set_addrs(self, addr: SocketAddr) -> Self {
385 self.listener_addr(addr).discovery_addr(addr)
386 }
387
388 pub const fn listener_addr(mut self, listener_addr: SocketAddr) -> Self {
392 self.listener_addr = Some(listener_addr);
393 self
394 }
395
396 pub fn listener_port(mut self, port: u16) -> Self {
400 self.listener_addr.get_or_insert(DEFAULT_DISCOVERY_ADDRESS).set_port(port);
401 self
402 }
403
404 pub const fn discovery_addr(mut self, discovery_addr: SocketAddr) -> Self {
406 self.discovery_addr = Some(discovery_addr);
407 self
408 }
409
410 pub fn discovery_port(mut self, port: u16) -> Self {
414 self.discovery_addr.get_or_insert(DEFAULT_DISCOVERY_ADDRESS).set_port(port);
415 self
416 }
417
418 pub fn with_unused_ports(self) -> Self {
421 self.with_unused_discovery_port().with_unused_listener_port()
422 }
423
424 pub fn with_unused_discovery_port(self) -> Self {
427 self.discovery_port(0)
428 }
429
430 pub fn with_unused_listener_port(self) -> Self {
433 self.listener_port(0)
434 }
435
436 pub fn external_ip_resolver(mut self, resolver: NatResolver) -> Self {
443 self.discovery_v4_builder
444 .get_or_insert_with(Discv4Config::builder)
445 .external_ip_resolver(Some(resolver.clone()));
446 self.nat = Some(resolver);
447 self
448 }
449
450 pub fn discovery(mut self, builder: Discv4ConfigBuilder) -> Self {
452 self.discovery_v4_builder = Some(builder);
453 self
454 }
455
456 pub fn discovery_v5(mut self, builder: reth_discv5::ConfigBuilder) -> Self {
458 self.discovery_v5_builder = Some(builder);
459 self
460 }
461
462 pub fn dns_discovery(mut self, config: DnsDiscoveryConfig) -> Self {
464 self.dns_discovery_config = Some(config);
465 self
466 }
467
468 pub fn mainnet_boot_nodes(self) -> Self {
470 self.boot_nodes(mainnet_nodes())
471 }
472
473 pub fn sepolia_boot_nodes(self) -> Self {
475 self.boot_nodes(sepolia_nodes())
476 }
477
478 pub fn boot_nodes<T: Into<TrustedPeer>>(mut self, nodes: impl IntoIterator<Item = T>) -> Self {
480 self.boot_nodes = nodes.into_iter().map(Into::into).collect();
481 self
482 }
483
484 pub fn boot_nodes_iter(&self) -> impl Iterator<Item = &TrustedPeer> + '_ {
486 self.boot_nodes.iter()
487 }
488
489 pub fn disable_dns_discovery(mut self) -> Self {
491 self.dns_discovery_config = None;
492 self
493 }
494
495 pub fn disable_nat(mut self) -> Self {
497 self.nat = None;
498 self
499 }
500
501 pub fn disable_discovery(self) -> Self {
503 self.disable_discv4_discovery().disable_discv5_discovery().disable_dns_discovery()
504 }
505
506 pub fn disable_discovery_if(self, disable: bool) -> Self {
508 if disable {
509 self.disable_discovery()
510 } else {
511 self
512 }
513 }
514
515 pub fn disable_discv4_discovery(mut self) -> Self {
517 self.discovery_v4_builder = None;
518 self
519 }
520
521 pub fn disable_discv5_discovery(mut self) -> Self {
523 self.discovery_v5_builder = None;
524 self
525 }
526
527 pub fn disable_dns_discovery_if(self, disable: bool) -> Self {
529 if disable {
530 self.disable_dns_discovery()
531 } else {
532 self
533 }
534 }
535
536 pub fn disable_discv4_discovery_if(self, disable: bool) -> Self {
538 if disable {
539 self.disable_discv4_discovery()
540 } else {
541 self
542 }
543 }
544
545 pub fn disable_discv5_discovery_if(self, disable: bool) -> Self {
547 if disable {
548 self.disable_discv5_discovery()
549 } else {
550 self
551 }
552 }
553
554 pub fn add_rlpx_sub_protocol(mut self, protocol: impl IntoRlpxSubProtocol) -> Self {
558 self.extra_protocols.push(protocol);
559 self
560 }
561
562 pub const fn with_snap(mut self, snap_enabled: bool) -> Self {
566 self.snap_enabled = snap_enabled;
567 self
568 }
569
570 pub const fn disable_tx_gossip(mut self, disable_tx_gossip: bool) -> Self {
572 self.tx_gossip_disabled = disable_tx_gossip;
573 self
574 }
575
576 pub fn required_block_hashes(mut self, hashes: Vec<BlockNumHash>) -> Self {
578 self.required_block_hashes = hashes;
579 self
580 }
581
582 pub fn block_import(mut self, block_import: Box<dyn BlockImport<N::NewBlockPayload>>) -> Self {
584 self.block_import = Some(block_import);
585 self
586 }
587
588 pub fn build_with_noop_provider<ChainSpec>(
591 self,
592 chain_spec: Arc<ChainSpec>,
593 ) -> NetworkConfig<NoopProvider<ChainSpec>, N>
594 where
595 ChainSpec: EthChainSpec + Hardforks + 'static,
596 {
597 self.build(NoopProvider::eth(chain_spec))
598 }
599
600 pub fn add_nat(mut self, nat: Option<NatResolver>) -> Self {
602 self.nat = nat;
603 self
604 }
605
606 pub fn eth_rlpx_handshake(mut self, handshake: Arc<dyn EthRlpxHandshake>) -> Self {
608 self.handshake = handshake;
609 self
610 }
611
612 pub const fn eth_max_message_size(mut self, max_message_size: usize) -> Self {
617 self.eth_max_message_size = max_message_size;
618 self
619 }
620
621 pub const fn eth_max_message_size_opt(mut self, max_message_size: Option<usize>) -> Self {
623 if let Some(max_message_size) = max_message_size {
624 self.eth_max_message_size = max_message_size;
625 }
626 self
627 }
628
629 pub const fn network_id(mut self, network_id: Option<u64>) -> Self {
631 self.network_id = network_id;
632 self
633 }
634
635 pub fn build<C>(self, client: C) -> NetworkConfig<C, N>
642 where
643 C: ChainSpecProvider<ChainSpec: Hardforks>,
644 {
645 let peer_id = self.get_peer_id();
646 let chain_spec = client.chain_spec();
647 let Self {
648 secret_key,
649 mut dns_discovery_config,
650 discovery_v4_builder,
651 mut discovery_v5_builder,
652 boot_nodes,
653 discovery_addr,
654 listener_addr,
655 peers_config,
656 sessions_config,
657 network_mode,
658 executor,
659 hello_message,
660 extra_protocols,
661 head,
662 tx_gossip_disabled,
663 block_import,
664 transactions_manager_config,
665 nat,
666 handshake,
667 eth_max_message_size,
668 required_block_hashes,
669 network_id,
670 snap_enabled,
671 } = self;
672
673 let head = head.unwrap_or_else(|| Head {
674 hash: chain_spec.genesis_hash(),
675 number: 0,
676 timestamp: chain_spec.genesis().timestamp,
677 difficulty: chain_spec.genesis().difficulty,
678 total_difficulty: chain_spec.genesis().difficulty,
679 });
680
681 let listener_addr = listener_addr.unwrap_or(DEFAULT_DISCOVERY_ADDRESS);
682 let advertised_ip = nat.clone().and_then(|nat| nat.as_external_ip(listener_addr.port()));
686
687 discovery_v5_builder = discovery_v5_builder.map(|mut builder| {
688 let fork_id = chain_spec.fork_id(&head);
689 if let Some(network_stack_id) = NetworkStackId::id(&chain_spec) {
690 builder = builder.fork(network_stack_id, fork_id)
691 } else {
692 builder = builder.fork_if_unset(NetworkStackId::ETH, fork_id)
695 }
696
697 if let Some(ip) = advertised_ip {
698 builder = builder.advertised_ip(ip);
699 }
700
701 builder
702 });
703
704 let mut hello_message =
705 hello_message.unwrap_or_else(|| HelloMessage::builder(peer_id).build());
706 hello_message.port = listener_addr.port();
707 hello_message = hello_message.with_snap(snap_enabled);
708
709 let mut status = UnifiedStatus::spec_builder(&chain_spec, &head);
711
712 if let Some(id) = network_id {
713 status.chain = id.into();
714 }
715
716 let fork_filter = chain_spec.fork_filter(head);
718
719 let chain_id = chain_spec.chain().id();
721
722 if let Some(dns_networks) =
724 dns_discovery_config.as_mut().and_then(|c| c.bootstrap_dns_networks.as_mut()) &&
725 dns_networks.is_empty() &&
726 let Some(link) = chain_spec.chain().public_dns_network_protocol()
727 {
728 dns_networks.insert(link.parse().expect("is valid DNS link entry"));
729 }
730
731 NetworkConfig {
732 client,
733 secret_key,
734 boot_nodes,
735 dns_discovery_config,
736 discovery_v4_config: discovery_v4_builder.map(|builder| builder.build()),
737 discovery_v5_config: discovery_v5_builder.map(|builder| builder.build()),
738 discovery_v4_addr: discovery_addr.unwrap_or(DEFAULT_DISCOVERY_ADDRESS),
739 listener_addr,
740 peers_config: peers_config.unwrap_or_default(),
741 sessions_config: sessions_config.unwrap_or_default(),
742 chain_id,
743 block_import: block_import.unwrap_or_else(|| Box::<ProofOfStakeBlockImport>::default()),
744 network_mode,
745 executor,
746 status,
747 hello_message,
748 extra_protocols,
749 fork_filter,
750 tx_gossip_disabled,
751 transactions_manager_config,
752 nat,
753 handshake,
754 eth_max_message_size,
755 required_block_hashes,
756 }
757 }
758}
759
760#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
766#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
767pub enum NetworkMode {
768 Work,
770 #[default]
772 Stake,
773}
774
775impl NetworkMode {
778 pub const fn is_stake(&self) -> bool {
780 matches!(self, Self::Stake)
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use alloy_eips::eip2124::ForkHash;
788 use alloy_genesis::Genesis;
789 use alloy_primitives::U256;
790 use reth_chainspec::{
791 Chain, ChainSpecBuilder, EthereumHardfork, ForkCondition, ForkId, MAINNET,
792 };
793 use reth_discv5::build_local_enr;
794 use reth_dns_discovery::tree::LinkEntry;
795 use reth_storage_api::noop::NoopProvider;
796 use std::{net::Ipv4Addr, sync::Arc};
797
798 fn builder() -> NetworkConfigBuilder {
799 let secret_key = SecretKey::new(&mut rand_08::thread_rng());
800 NetworkConfigBuilder::new(secret_key, Runtime::test())
801 }
802
803 #[test]
804 fn test_snap_advertisement_default_off() {
805 let config = builder().build(NoopProvider::default());
807 assert!(config.hello_message.protocols.iter().all(|p| p.cap.name != "snap"));
808 }
809
810 #[test]
811 fn test_snap_advertisement_when_enabled() {
812 let config = builder().with_snap(true).build(NoopProvider::default());
813 let snap_caps =
814 config.hello_message.protocols.iter().filter(|p| p.cap.name == "snap").count();
815 assert_eq!(snap_caps, 1);
816 assert_eq!(
817 config
818 .hello_message
819 .protocols
820 .iter()
821 .find(|p| p.cap.name == "snap")
822 .unwrap()
823 .cap
824 .version,
825 2
826 );
827 assert!(config.hello_message.protocols.iter().any(|p| p.cap.name == "eth"));
829 }
830
831 #[test]
832 fn test_network_dns_defaults() {
833 let config = builder().build(NoopProvider::default());
834
835 let dns = config.dns_discovery_config.unwrap();
836 let bootstrap_nodes = dns.bootstrap_dns_networks.unwrap();
837 let mainnet_dns: LinkEntry =
838 Chain::mainnet().public_dns_network_protocol().unwrap().parse().unwrap();
839 assert!(bootstrap_nodes.contains(&mainnet_dns));
840 assert_eq!(bootstrap_nodes.len(), 1);
841 }
842
843 #[test]
844 fn test_network_fork_filter_default() {
845 let mut chain_spec = Arc::clone(&MAINNET);
846
847 Arc::make_mut(&mut chain_spec).hardforks = Default::default();
849
850 let genesis_fork_hash = ForkHash::from(chain_spec.genesis_hash());
852
853 let config = builder().build_with_noop_provider(chain_spec);
855
856 let status = config.status;
857 let fork_filter = config.fork_filter;
858
859 assert_eq!(status.forkid.next, 0);
861
862 assert_eq!(fork_filter.current().next, 0);
864
865 assert_eq!(status.forkid.hash, genesis_fork_hash);
867 assert_eq!(fork_filter.current().hash, genesis_fork_hash);
868 }
869
870 #[test]
871 fn test_discv5_fork_id_default() {
872 const GENESIS_TIME: u64 = 151_515;
873
874 let genesis = Genesis::default().with_timestamp(GENESIS_TIME);
875
876 let active_fork = (EthereumHardfork::Shanghai, ForkCondition::Timestamp(GENESIS_TIME));
877 let future_fork = (EthereumHardfork::Cancun, ForkCondition::Timestamp(GENESIS_TIME + 1));
878
879 let chain_spec = ChainSpecBuilder::default()
880 .chain(Chain::dev())
881 .genesis(genesis)
882 .with_fork(active_fork.0, active_fork.1)
883 .with_fork(future_fork.0, future_fork.1)
884 .build();
885
886 let genesis_fork_hash = ForkHash::from(chain_spec.genesis_hash());
888 let fork_id = ForkId { hash: genesis_fork_hash, next: GENESIS_TIME + 1 };
889 assert_eq!(
891 fork_id,
892 chain_spec.fork_id(&Head {
893 hash: chain_spec.genesis_hash(),
894 number: 0,
895 timestamp: GENESIS_TIME,
896 difficulty: U256::ZERO,
897 total_difficulty: U256::ZERO,
898 })
899 );
900 assert_ne!(fork_id, chain_spec.latest_fork_id());
901
902 let fork_key = b"odyssey";
904 let config = builder()
905 .discovery_v5(
906 reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 30303).into())
907 .fork(fork_key, fork_id),
908 )
909 .build_with_noop_provider(Arc::new(chain_spec));
910
911 let (local_enr, _, _, _) = build_local_enr(
912 &config.secret_key,
913 &config.discovery_v5_config.expect("should build config"),
914 );
915
916 let advertised_fork_id = *local_enr
919 .get_decodable::<Vec<ForkId>>(fork_key)
920 .expect("should read 'odyssey'")
921 .expect("should decode fork id list")
922 .first()
923 .expect("should be non-empty");
924
925 assert_eq!(advertised_fork_id, fork_id);
926 }
927
928 #[test]
929 fn test_discv5_fork_id_custom_chain_id_fallback() {
930 const GENESIS_TIME: u64 = 151_515;
931
932 let genesis = Genesis::default().with_timestamp(GENESIS_TIME);
933
934 let chain_spec = ChainSpecBuilder::default()
935 .chain(Chain::from_id(3151908))
936 .genesis(genesis)
937 .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(GENESIS_TIME))
938 .build();
939
940 let fork_id = chain_spec.fork_id(&Head {
941 hash: chain_spec.genesis_hash(),
942 number: 0,
943 timestamp: GENESIS_TIME,
944 difficulty: U256::ZERO,
945 total_difficulty: U256::ZERO,
946 });
947
948 let config = builder()
949 .discovery_v5(reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 30303).into()))
950 .build_with_noop_provider(Arc::new(chain_spec));
951
952 let (local_enr, _, _, _) = build_local_enr(
953 &config.secret_key,
954 &config.discovery_v5_config.expect("should build config"),
955 );
956
957 let advertised_fork_id = *local_enr
958 .get_decodable::<Vec<ForkId>>(NetworkStackId::ETH)
959 .expect("should read 'eth'")
960 .expect("should decode fork id list")
961 .first()
962 .expect("should be non-empty");
963
964 assert_eq!(advertised_fork_id, fork_id);
965 }
966}