Skip to main content

reth_network/
config.rs

1//! Network config support
2
3use 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, SnapVersion};
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, StateProviderFactory,
25    StateRangeProviderFactory,
26};
27use reth_tasks::Runtime;
28use secp256k1::SECP256K1;
29use std::{collections::HashSet, net::SocketAddr, sync::Arc};
30
31// re-export for convenience
32use crate::{
33    protocol::{IntoRlpxSubProtocol, RlpxSubProtocols},
34    transactions::TransactionPropagationMode,
35};
36pub use secp256k1::SecretKey;
37
38/// Convenience function to create a new random [`SecretKey`]
39pub fn rng_secret_key() -> SecretKey {
40    SecretKey::new(&mut rand_08::thread_rng())
41}
42
43/// All network related initialization settings.
44#[derive(Debug)]
45pub struct NetworkConfig<C, N: NetworkPrimitives = EthNetworkPrimitives> {
46    /// The client type that can interact with the chain.
47    ///
48    /// This type is used to fetch the block number after we established a session and received the
49    /// [`UnifiedStatus`] block hash.
50    pub client: C,
51    /// The node's secret key, from which the node's identity is derived.
52    pub secret_key: SecretKey,
53    /// All boot nodes to start network discovery with.
54    pub boot_nodes: HashSet<TrustedPeer>,
55    /// How to set up discovery over DNS.
56    pub dns_discovery_config: Option<DnsDiscoveryConfig>,
57    /// Address to use for discovery v4.
58    pub discovery_v4_addr: SocketAddr,
59    /// How to set up discovery.
60    pub discovery_v4_config: Option<Discv4Config>,
61    /// How to set up discovery version 5.
62    pub discovery_v5_config: Option<reth_discv5::Config>,
63    /// Address to listen for incoming connections
64    pub listener_addr: SocketAddr,
65    /// How to instantiate peer manager.
66    pub peers_config: PeersConfig,
67    /// How to configure the [`SessionManager`](crate::session::SessionManager).
68    pub sessions_config: SessionsConfig,
69    /// The chain id
70    pub chain_id: u64,
71    /// The [`ForkFilter`] to use at launch for authenticating sessions.
72    ///
73    /// See also <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2124.md#stale-software-examples>
74    ///
75    /// For sync from block `0`, this should be the default chain [`ForkFilter`] beginning at the
76    /// first hardfork, `Frontier` for mainnet.
77    pub fork_filter: ForkFilter,
78    /// The block importer type.
79    pub block_import: Box<dyn BlockImport<N::NewBlockPayload>>,
80    /// The default mode of the network.
81    pub network_mode: NetworkMode,
82    /// The executor to use for spawning tasks.
83    pub executor: Runtime,
84    /// The `Status` message to send to peers at the beginning.
85    pub status: UnifiedStatus,
86    /// Sets the hello message for the p2p handshake in `RLPx`
87    pub hello_message: HelloMessageWithProtocols,
88    /// Additional `RLPx` sub-protocols to announce and handle alongside `eth`.
89    ///
90    /// Does not cover `snap/2`, which is supported natively (see
91    /// [`NetworkConfigBuilder::with_snap`]).
92    pub extra_protocols: RlpxSubProtocols,
93    /// Whether to disable transaction gossip
94    pub tx_gossip_disabled: bool,
95    /// How to instantiate transactions manager.
96    pub transactions_manager_config: TransactionsManagerConfig,
97    /// The NAT resolver for external IP
98    pub nat: Option<NatResolver>,
99    /// The Ethereum P2P handshake, see also:
100    /// <https://github.com/ethereum/devp2p/blob/master/rlpx.md#initial-handshake>.
101    /// This can be overridden to support custom handshake logic via the
102    /// [`NetworkConfigBuilder`].
103    pub handshake: Arc<dyn EthRlpxHandshake>,
104    /// Maximum allowed ETH message size for post-handshake ETH/Snap streams.
105    pub eth_max_message_size: usize,
106    /// List of block number-hash pairs to check for required blocks.
107    /// If non-empty, peers that don't have these blocks will be filtered out.
108    pub required_block_hashes: Vec<BlockNumHash>,
109}
110
111// === impl NetworkConfig ===
112
113impl<N: NetworkPrimitives> NetworkConfig<(), N> {
114    /// Convenience method for creating the corresponding builder type.
115    pub fn builder(secret_key: SecretKey, executor: Runtime) -> NetworkConfigBuilder<N> {
116        NetworkConfigBuilder::new(secret_key, executor)
117    }
118
119    /// Convenience method for creating the corresponding builder type with a random secret key.
120    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    /// Apply a function to the config.
127    pub fn apply<F>(self, f: F) -> Self
128    where
129        F: FnOnce(Self) -> Self,
130    {
131        f(self)
132    }
133
134    /// Sets the config to use for the discovery v4 protocol.
135    pub fn set_discovery_v4(mut self, discovery_config: Discv4Config) -> Self {
136        self.discovery_v4_config = Some(discovery_config);
137        self
138    }
139
140    /// Sets the address for the incoming `RLPx` connection listener.
141    pub const fn set_listener_addr(mut self, listener_addr: SocketAddr) -> Self {
142        self.listener_addr = listener_addr;
143        self
144    }
145
146    /// Returns the address for the incoming `RLPx` connection listener.
147    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    /// Convenience method for calling [`NetworkManager::new`].
158    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        + Clone
171        + Unpin
172        + 'static,
173{
174    /// Starts the networking stack given a [`NetworkConfig`] and returns a handle to the network.
175    pub async fn start_network(self) -> Result<NetworkHandle<N>, NetworkError> {
176        let client = self.client.clone();
177        let (handle, network, _txpool, eth) = NetworkManager::builder::<C>(self)
178            .await?
179            .request_handler::<C>(client)
180            .split_with_handle();
181
182        tokio::task::spawn(network);
183        tokio::task::spawn(eth);
184        Ok(handle)
185    }
186}
187
188/// Builder for [`NetworkConfig`].
189#[derive(Debug)]
190pub struct NetworkConfigBuilder<N: NetworkPrimitives = EthNetworkPrimitives> {
191    /// The node's secret key, from which the node's identity is derived.
192    secret_key: SecretKey,
193    /// How to configure discovery over DNS.
194    dns_discovery_config: Option<DnsDiscoveryConfig>,
195    /// How to set up discovery version 4.
196    discovery_v4_builder: Option<Discv4ConfigBuilder>,
197    /// How to set up discovery version 5.
198    discovery_v5_builder: Option<reth_discv5::ConfigBuilder>,
199    /// All boot nodes to start network discovery with.
200    boot_nodes: HashSet<TrustedPeer>,
201    /// Address to use for discovery
202    discovery_addr: Option<SocketAddr>,
203    /// Listener for incoming connections
204    listener_addr: Option<SocketAddr>,
205    /// How to instantiate peer manager.
206    peers_config: Option<PeersConfig>,
207    /// How to configure the sessions manager
208    sessions_config: Option<SessionsConfig>,
209    /// The default mode of the network.
210    network_mode: NetworkMode,
211    /// The executor to use for spawning tasks.
212    executor: Runtime,
213    /// Sets the hello message for the p2p handshake in `RLPx`
214    hello_message: Option<HelloMessageWithProtocols>,
215    /// Additional `RLPx` sub-protocols to announce and handle alongside `eth`. Does not cover
216    /// `snap/2`, which is supported natively (see [`NetworkConfigBuilder::with_snap`]).
217    extra_protocols: RlpxSubProtocols,
218    /// Head used to start set for the fork filter and status.
219    head: Option<Head>,
220    /// Whether tx gossip is disabled
221    tx_gossip_disabled: bool,
222    /// The block importer type
223    block_import: Option<Box<dyn BlockImport<N::NewBlockPayload>>>,
224    /// How to instantiate transactions manager.
225    transactions_manager_config: TransactionsManagerConfig,
226    /// The NAT resolver for external IP
227    nat: Option<NatResolver>,
228    /// The Ethereum P2P handshake, see also:
229    /// <https://github.com/ethereum/devp2p/blob/master/rlpx.md#initial-handshake>.
230    handshake: Arc<dyn EthRlpxHandshake>,
231    /// Maximum allowed ETH message size for post-handshake ETH/Snap streams.
232    eth_max_message_size: usize,
233    /// List of block hashes to check for required blocks.
234    required_block_hashes: Vec<BlockNumHash>,
235    /// Optional network id
236    network_id: Option<u64>,
237    /// Whether to advertise the `snap/2` satellite protocol (EIP-8189) in the handshake.
238    snap_enabled: bool,
239}
240
241impl NetworkConfigBuilder<EthNetworkPrimitives> {
242    /// Creates the `NetworkConfigBuilder` with [`EthNetworkPrimitives`] types.
243    pub fn eth(secret_key: SecretKey, executor: Runtime) -> Self {
244        Self::new(secret_key, executor)
245    }
246}
247
248// === impl NetworkConfigBuilder ===
249
250#[expect(missing_docs)]
251impl<N: NetworkPrimitives> NetworkConfigBuilder<N> {
252    /// Create a new builder instance with a random secret key.
253    pub fn with_rng_secret_key(executor: Runtime) -> Self {
254        Self::new(rng_secret_key(), executor)
255    }
256
257    /// Create a new builder instance with the given secret key.
258    pub fn new(secret_key: SecretKey, executor: Runtime) -> Self {
259        Self {
260            secret_key,
261            dns_discovery_config: Some(Default::default()),
262            discovery_v4_builder: Some(Default::default()),
263            discovery_v5_builder: None,
264            boot_nodes: Default::default(),
265            discovery_addr: None,
266            listener_addr: None,
267            peers_config: None,
268            sessions_config: None,
269            network_mode: Default::default(),
270            executor,
271            hello_message: None,
272            extra_protocols: Default::default(),
273            head: None,
274            tx_gossip_disabled: false,
275            block_import: None,
276            transactions_manager_config: Default::default(),
277            nat: None,
278            handshake: Arc::new(EthHandshake::default()),
279            eth_max_message_size: MAX_MESSAGE_SIZE,
280            required_block_hashes: Vec::new(),
281            network_id: None,
282            snap_enabled: false,
283        }
284    }
285
286    /// Apply a function to the builder.
287    pub fn apply<F>(self, f: F) -> Self
288    where
289        F: FnOnce(Self) -> Self,
290    {
291        f(self)
292    }
293
294    /// Returns the configured [`PeerId`]
295    pub fn get_peer_id(&self) -> PeerId {
296        pk2id(&self.secret_key.public_key(SECP256K1))
297    }
298
299    /// Returns the configured [`SecretKey`], from which the node's identity is derived.
300    pub const fn secret_key(&self) -> &SecretKey {
301        &self.secret_key
302    }
303
304    /// Sets the [`NetworkMode`].
305    pub const fn network_mode(mut self, network_mode: NetworkMode) -> Self {
306        self.network_mode = network_mode;
307        self
308    }
309
310    /// Configures the network to use proof-of-work.
311    ///
312    /// This effectively allows block propagation in the `eth` sub-protocol, which has been
313    /// soft-deprecated with ethereum `PoS` after the merge. Even if block propagation is
314    /// technically allowed, according to the eth protocol, it is not expected to be used in `PoS`
315    /// networks and peers are supposed to terminate the connection if they receive a `NewBlock`
316    /// message.
317    pub const fn with_pow(self) -> Self {
318        self.network_mode(NetworkMode::Work)
319    }
320
321    /// Sets the highest synced block.
322    ///
323    /// This is used to construct the appropriate [`ForkFilter`] and [`UnifiedStatus`] message.
324    ///
325    /// If not set, this defaults to the genesis specified by the current chain specification.
326    pub const fn set_head(mut self, head: Head) -> Self {
327        self.head = Some(head);
328        self
329    }
330
331    /// Sets the `HelloMessage` to send when connecting to peers.
332    ///
333    /// ```
334    /// # use reth_eth_wire::HelloMessage;
335    /// # use reth_network::NetworkConfigBuilder;
336    /// # fn builder(builder: NetworkConfigBuilder) {
337    /// let peer_id = builder.get_peer_id();
338    /// builder.hello_message(HelloMessage::builder(peer_id).build());
339    /// # }
340    /// ```
341    pub fn hello_message(mut self, hello_message: HelloMessageWithProtocols) -> Self {
342        self.hello_message = Some(hello_message);
343        self
344    }
345
346    /// Set a custom peer config for how peers are handled
347    pub fn peer_config(mut self, config: PeersConfig) -> Self {
348        self.peers_config = Some(config);
349        self
350    }
351
352    /// Sets the executor to use for spawning tasks.
353    pub fn with_task_executor(mut self, executor: Runtime) -> Self {
354        self.executor = executor;
355        self
356    }
357
358    /// Sets a custom config for how sessions are handled.
359    pub const fn sessions_config(mut self, config: SessionsConfig) -> Self {
360        self.sessions_config = Some(config);
361        self
362    }
363
364    /// Configures the transactions manager with the given config.
365    pub const fn transactions_manager_config(mut self, config: TransactionsManagerConfig) -> Self {
366        self.transactions_manager_config = config;
367        self
368    }
369
370    /// Configures the propagation mode for the transaction manager.
371    pub const fn transaction_propagation_mode(mut self, mode: TransactionPropagationMode) -> Self {
372        self.transactions_manager_config.propagation_mode = mode;
373        self
374    }
375
376    /// Sets the discovery and listener address
377    ///
378    /// This is a convenience function for both [`NetworkConfigBuilder::listener_addr`] and
379    /// [`NetworkConfigBuilder::discovery_addr`].
380    ///
381    /// By default, both are on the same port:
382    /// [`DEFAULT_DISCOVERY_PORT`](reth_discv4::DEFAULT_DISCOVERY_PORT)
383    pub const fn set_addrs(self, addr: SocketAddr) -> Self {
384        self.listener_addr(addr).discovery_addr(addr)
385    }
386
387    /// Sets the socket address the network will listen on.
388    ///
389    /// By default, this is [`DEFAULT_DISCOVERY_ADDRESS`]
390    pub const fn listener_addr(mut self, listener_addr: SocketAddr) -> Self {
391        self.listener_addr = Some(listener_addr);
392        self
393    }
394
395    /// Sets the port of the address the network will listen on.
396    ///
397    /// By default, this is [`DEFAULT_DISCOVERY_PORT`](reth_discv4::DEFAULT_DISCOVERY_PORT)
398    pub fn listener_port(mut self, port: u16) -> Self {
399        self.listener_addr.get_or_insert(DEFAULT_DISCOVERY_ADDRESS).set_port(port);
400        self
401    }
402
403    /// Sets the socket address the discovery network will listen on
404    pub const fn discovery_addr(mut self, discovery_addr: SocketAddr) -> Self {
405        self.discovery_addr = Some(discovery_addr);
406        self
407    }
408
409    /// Sets the port of the address the discovery network will listen on.
410    ///
411    /// By default, this is [`DEFAULT_DISCOVERY_PORT`](reth_discv4::DEFAULT_DISCOVERY_PORT)
412    pub fn discovery_port(mut self, port: u16) -> Self {
413        self.discovery_addr.get_or_insert(DEFAULT_DISCOVERY_ADDRESS).set_port(port);
414        self
415    }
416
417    /// Launches the network with an unused network and discovery port
418    /// This is useful for testing.
419    pub fn with_unused_ports(self) -> Self {
420        self.with_unused_discovery_port().with_unused_listener_port()
421    }
422
423    /// Sets the discovery port to an unused port.
424    /// This is useful for testing.
425    pub fn with_unused_discovery_port(self) -> Self {
426        self.discovery_port(0)
427    }
428
429    /// Sets the listener port to an unused port.
430    /// This is useful for testing.
431    pub fn with_unused_listener_port(self) -> Self {
432        self.listener_port(0)
433    }
434
435    /// Sets the external ip resolver to use for discovery v4.
436    ///
437    /// If no [`Discv4ConfigBuilder`] is set via [`Self::discovery`], this will create a new one.
438    ///
439    /// This is a convenience function for setting the external ip resolver on the default
440    /// [`Discv4Config`] config.
441    pub fn external_ip_resolver(mut self, resolver: NatResolver) -> Self {
442        self.discovery_v4_builder
443            .get_or_insert_with(Discv4Config::builder)
444            .external_ip_resolver(Some(resolver.clone()));
445        self.nat = Some(resolver);
446        self
447    }
448
449    /// Sets the discv4 config to use.
450    pub fn discovery(mut self, builder: Discv4ConfigBuilder) -> Self {
451        self.discovery_v4_builder = Some(builder);
452        self
453    }
454
455    /// Sets the discv5 config to use.
456    pub fn discovery_v5(mut self, builder: reth_discv5::ConfigBuilder) -> Self {
457        self.discovery_v5_builder = Some(builder);
458        self
459    }
460
461    /// Sets the dns discovery config to use.
462    pub fn dns_discovery(mut self, config: DnsDiscoveryConfig) -> Self {
463        self.dns_discovery_config = Some(config);
464        self
465    }
466
467    /// Convenience function for setting [`Self::boot_nodes`] to the mainnet boot nodes.
468    pub fn mainnet_boot_nodes(self) -> Self {
469        self.boot_nodes(mainnet_nodes())
470    }
471
472    /// Convenience function for setting [`Self::boot_nodes`] to the sepolia boot nodes.
473    pub fn sepolia_boot_nodes(self) -> Self {
474        self.boot_nodes(sepolia_nodes())
475    }
476
477    /// Sets the boot nodes to use to bootstrap the configured discovery services (discv4 + discv5).
478    pub fn boot_nodes<T: Into<TrustedPeer>>(mut self, nodes: impl IntoIterator<Item = T>) -> Self {
479        self.boot_nodes = nodes.into_iter().map(Into::into).collect();
480        self
481    }
482
483    /// Returns an iterator over all configured boot nodes.
484    pub fn boot_nodes_iter(&self) -> impl Iterator<Item = &TrustedPeer> + '_ {
485        self.boot_nodes.iter()
486    }
487
488    /// Disable the DNS discovery.
489    pub fn disable_dns_discovery(mut self) -> Self {
490        self.dns_discovery_config = None;
491        self
492    }
493
494    // Disable nat
495    pub fn disable_nat(mut self) -> Self {
496        self.nat = None;
497        self
498    }
499
500    /// Disables all discovery.
501    pub fn disable_discovery(self) -> Self {
502        self.disable_discv4_discovery().disable_discv5_discovery().disable_dns_discovery()
503    }
504
505    /// Disables all discovery if the given condition is true.
506    pub fn disable_discovery_if(self, disable: bool) -> Self {
507        if disable {
508            self.disable_discovery()
509        } else {
510            self
511        }
512    }
513
514    /// Disable the Discv4 discovery.
515    pub fn disable_discv4_discovery(mut self) -> Self {
516        self.discovery_v4_builder = None;
517        self
518    }
519
520    /// Disable the Discv5 discovery.
521    pub fn disable_discv5_discovery(mut self) -> Self {
522        self.discovery_v5_builder = None;
523        self
524    }
525
526    /// Disable the DNS discovery if the given condition is true.
527    pub fn disable_dns_discovery_if(self, disable: bool) -> Self {
528        if disable {
529            self.disable_dns_discovery()
530        } else {
531            self
532        }
533    }
534
535    /// Disable the Discv4 discovery if the given condition is true.
536    pub fn disable_discv4_discovery_if(self, disable: bool) -> Self {
537        if disable {
538            self.disable_discv4_discovery()
539        } else {
540            self
541        }
542    }
543
544    /// Disable the Discv5 discovery if the given condition is true.
545    pub fn disable_discv5_discovery_if(self, disable: bool) -> Self {
546        if disable {
547            self.disable_discv5_discovery()
548        } else {
549            self
550        }
551    }
552
553    /// Adds a new additional protocol to the `RLPx` sub-protocol list.
554    ///
555    /// Not for `snap/2`, which is supported natively (see [`Self::with_snap`]).
556    pub fn add_rlpx_sub_protocol(mut self, protocol: impl IntoRlpxSubProtocol) -> Self {
557        self.extra_protocols.push(protocol);
558        self
559    }
560
561    /// Toggles advertisement of the `snap/2` satellite protocol (EIP-8189).
562    ///
563    /// Default off: snap/2 is only negotiated with peers when explicitly enabled.
564    pub const fn with_snap(mut self, snap_enabled: bool) -> Self {
565        self.snap_enabled = snap_enabled;
566        self
567    }
568
569    /// Sets whether tx gossip is disabled.
570    pub const fn disable_tx_gossip(mut self, disable_tx_gossip: bool) -> Self {
571        self.tx_gossip_disabled = disable_tx_gossip;
572        self
573    }
574
575    /// Sets the required block hashes for peer filtering.
576    pub fn required_block_hashes(mut self, hashes: Vec<BlockNumHash>) -> Self {
577        self.required_block_hashes = hashes;
578        self
579    }
580
581    /// Sets the block import type.
582    pub fn block_import(mut self, block_import: Box<dyn BlockImport<N::NewBlockPayload>>) -> Self {
583        self.block_import = Some(block_import);
584        self
585    }
586
587    /// Convenience function for creating a [`NetworkConfig`] with a noop provider that does
588    /// nothing.
589    pub fn build_with_noop_provider<ChainSpec>(
590        self,
591        chain_spec: Arc<ChainSpec>,
592    ) -> NetworkConfig<NoopProvider<ChainSpec>, N>
593    where
594        ChainSpec: EthChainSpec + Hardforks + 'static,
595    {
596        self.build(NoopProvider::eth(chain_spec))
597    }
598
599    /// Sets the NAT resolver for external IP.
600    pub fn add_nat(mut self, nat: Option<NatResolver>) -> Self {
601        self.nat = nat;
602        self
603    }
604
605    /// Overrides the default Eth `RLPx` handshake.
606    pub fn eth_rlpx_handshake(mut self, handshake: Arc<dyn EthRlpxHandshake>) -> Self {
607        self.handshake = handshake;
608        self
609    }
610
611    /// Sets the maximum allowed ETH message size for post-handshake ETH/Snap streams.
612    ///
613    /// This does not affect the initial status handshake, which continues to use
614    /// [`MAX_MESSAGE_SIZE`].
615    pub const fn eth_max_message_size(mut self, max_message_size: usize) -> Self {
616        self.eth_max_message_size = max_message_size;
617        self
618    }
619
620    /// Sets the maximum allowed ETH message size for post-handshake ETH/Snap streams if present.
621    pub const fn eth_max_message_size_opt(mut self, max_message_size: Option<usize>) -> Self {
622        if let Some(max_message_size) = max_message_size {
623            self.eth_max_message_size = max_message_size;
624        }
625        self
626    }
627
628    /// Set the optional network id.
629    pub const fn network_id(mut self, network_id: Option<u64>) -> Self {
630        self.network_id = network_id;
631        self
632    }
633
634    /// Consumes the type and creates the actual [`NetworkConfig`]
635    /// for the given client type that can interact with the chain.
636    ///
637    /// The given client is to be used for interacting with the chain, for example fetching the
638    /// corresponding block for a given block hash we receive from a peer in the status message when
639    /// establishing a connection.
640    pub fn build<C>(self, client: C) -> NetworkConfig<C, N>
641    where
642        C: ChainSpecProvider<ChainSpec: Hardforks>,
643    {
644        let peer_id = self.get_peer_id();
645        let chain_spec = client.chain_spec();
646        let Self {
647            secret_key,
648            mut dns_discovery_config,
649            mut discovery_v4_builder,
650            mut discovery_v5_builder,
651            boot_nodes,
652            discovery_addr,
653            listener_addr,
654            peers_config,
655            sessions_config,
656            network_mode,
657            executor,
658            hello_message,
659            extra_protocols,
660            head,
661            tx_gossip_disabled,
662            block_import,
663            transactions_manager_config,
664            nat,
665            handshake,
666            eth_max_message_size,
667            required_block_hashes,
668            network_id,
669            snap_enabled,
670        } = self;
671
672        let head = head.unwrap_or_else(|| Head {
673            hash: chain_spec.genesis_hash(),
674            number: 0,
675            timestamp: chain_spec.genesis().timestamp,
676            difficulty: chain_spec.genesis().difficulty,
677            total_difficulty: chain_spec.genesis().difficulty,
678        });
679
680        let listener_addr = listener_addr.unwrap_or(DEFAULT_DISCOVERY_ADDRESS);
681        // Static NAT addresses (`extip`/`extaddr`) tell peers which IP to dial, but that IP may
682        // not exist on a local interface. Keep binding to `listener_addr` and use the NAT IP only
683        // as the ENR address.
684        let advertised_ip = nat.clone().and_then(|nat| nat.as_external_ip(listener_addr.port()));
685
686        // EIP-8189: DNS discovery lists select snap servers by the versions in this ENR entry.
687        let snap_versions = snap_enabled.then(|| vec![SnapVersion::V2 as u64]);
688        if let (Some(builder), Some(versions)) = (discovery_v4_builder.as_mut(), &snap_versions) {
689            builder.add_eip868_pair("snap", versions);
690        }
691
692        discovery_v5_builder = discovery_v5_builder.map(|mut builder| {
693            let fork_id = chain_spec.fork_id(&head);
694            if let Some(network_stack_id) = NetworkStackId::id(&chain_spec) {
695                builder = builder.fork(network_stack_id, fork_id)
696            } else {
697                // Custom Ethereum chains are not recognized by `NetworkStackId::id`, but still
698                // use the `eth` key for fork-aware discovery. Preserve any explicit override.
699                builder = builder.fork_if_unset(NetworkStackId::ETH, fork_id)
700            }
701
702            if let Some(ip) = advertised_ip {
703                builder = builder.advertised_ip(ip);
704            }
705
706            if let Some(versions) = &snap_versions {
707                builder = builder.add_enr_kv_pair(b"snap", alloy_rlp::encode(versions).into());
708            }
709
710            builder
711        });
712
713        let mut hello_message =
714            hello_message.unwrap_or_else(|| HelloMessage::builder(peer_id).build());
715        hello_message.port = listener_addr.port();
716        hello_message = hello_message.with_snap(snap_enabled);
717
718        // set the status
719        let mut status = UnifiedStatus::spec_builder(&chain_spec, &head);
720
721        if let Some(id) = network_id {
722            status.chain = id.into();
723        }
724
725        // set a fork filter based on the chain spec and head
726        let fork_filter = chain_spec.fork_filter(head);
727
728        // get the chain id
729        let chain_id = chain_spec.chain().id();
730
731        // If default DNS config is used then we add the known dns network to bootstrap from
732        if let Some(dns_networks) =
733            dns_discovery_config.as_mut().and_then(|c| c.bootstrap_dns_networks.as_mut()) &&
734            dns_networks.is_empty() &&
735            let Some(link) = chain_spec.chain().public_dns_network_protocol()
736        {
737            dns_networks.insert(link.parse().expect("is valid DNS link entry"));
738        }
739
740        NetworkConfig {
741            client,
742            secret_key,
743            boot_nodes,
744            dns_discovery_config,
745            discovery_v4_config: discovery_v4_builder.map(|builder| builder.build()),
746            discovery_v5_config: discovery_v5_builder.map(|builder| builder.build()),
747            discovery_v4_addr: discovery_addr.unwrap_or(DEFAULT_DISCOVERY_ADDRESS),
748            listener_addr,
749            peers_config: peers_config.unwrap_or_default(),
750            sessions_config: sessions_config.unwrap_or_default(),
751            chain_id,
752            block_import: block_import.unwrap_or_else(|| Box::<ProofOfStakeBlockImport>::default()),
753            network_mode,
754            executor,
755            status,
756            hello_message,
757            extra_protocols,
758            fork_filter,
759            tx_gossip_disabled,
760            transactions_manager_config,
761            nat,
762            handshake,
763            eth_max_message_size,
764            required_block_hashes,
765        }
766    }
767}
768
769/// Describes the mode of the network wrt. POS or POW.
770///
771/// This affects block propagation in the `eth` sub-protocol [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#devp2p)
772///
773/// In POS `NewBlockHashes` and `NewBlock` messages become invalid.
774#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
775#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
776pub enum NetworkMode {
777    /// Network is in proof-of-work mode.
778    Work,
779    /// Network is in proof-of-stake mode
780    #[default]
781    Stake,
782}
783
784// === impl NetworkMode ===
785
786impl NetworkMode {
787    /// Returns true if network has entered proof-of-stake
788    pub const fn is_stake(&self) -> bool {
789        matches!(self, Self::Stake)
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796    use alloy_eips::eip2124::ForkHash;
797    use alloy_genesis::Genesis;
798    use alloy_primitives::U256;
799    use reth_chainspec::{
800        Chain, ChainSpecBuilder, EthereumHardfork, ForkCondition, ForkId, MAINNET,
801    };
802    use reth_discv5::build_local_enr;
803    use reth_dns_discovery::tree::LinkEntry;
804    use reth_storage_api::noop::NoopProvider;
805    use std::{net::Ipv4Addr, sync::Arc};
806
807    fn builder() -> NetworkConfigBuilder {
808        let secret_key = SecretKey::new(&mut rand_08::thread_rng());
809        NetworkConfigBuilder::new(secret_key, Runtime::test())
810    }
811
812    #[test]
813    fn test_snap_advertisement_default_off() {
814        // snap/2 must not be advertised unless explicitly enabled.
815        let config = builder().build(NoopProvider::default());
816        assert!(config.hello_message.protocols.iter().all(|p| p.cap.name != "snap"));
817    }
818
819    #[test]
820    fn test_snap_advertisement_when_enabled() {
821        let config = builder().with_snap(true).build(NoopProvider::default());
822        let snap_caps =
823            config.hello_message.protocols.iter().filter(|p| p.cap.name == "snap").count();
824        assert_eq!(snap_caps, 1);
825        assert_eq!(
826            config
827                .hello_message
828                .protocols
829                .iter()
830                .find(|p| p.cap.name == "snap")
831                .unwrap()
832                .cap
833                .version,
834            2
835        );
836        // eth is still advertised alongside snap.
837        assert!(config.hello_message.protocols.iter().any(|p| p.cap.name == "eth"));
838    }
839
840    #[test]
841    fn test_snap_enr_entry_lists_served_versions() {
842        let config = builder()
843            .with_snap(true)
844            .discovery_v5(reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 30303).into()))
845            .build(NoopProvider::default());
846
847        let discv4 = config.discovery_v4_config.expect("should build discv4 config");
848        assert_eq!(
849            discv4.additional_eip868_rlp_pairs.get(b"snap".as_slice()).map(|rlp| rlp.as_ref()),
850            Some([0xc1, 0x02].as_slice())
851        );
852
853        let (local_enr, _, _, _) = build_local_enr(
854            &config.secret_key,
855            &config.discovery_v5_config.expect("should build discv5 config"),
856        );
857        assert_eq!(
858            local_enr.get_decodable::<Vec<u64>>("snap").expect("should read 'snap'").unwrap(),
859            vec![2]
860        );
861    }
862
863    #[test]
864    fn test_snap_enr_entry_absent_by_default() {
865        let config = builder()
866            .discovery_v5(reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 30303).into()))
867            .build(NoopProvider::default());
868
869        let discv4 = config.discovery_v4_config.expect("should build discv4 config");
870        assert!(!discv4.additional_eip868_rlp_pairs.contains_key(b"snap".as_slice()));
871
872        let (local_enr, _, _, _) = build_local_enr(
873            &config.secret_key,
874            &config.discovery_v5_config.expect("should build discv5 config"),
875        );
876        assert!(local_enr.get_raw_rlp("snap").is_none());
877    }
878
879    #[test]
880    fn test_network_dns_defaults() {
881        let config = builder().build(NoopProvider::default());
882
883        let dns = config.dns_discovery_config.unwrap();
884        let bootstrap_nodes = dns.bootstrap_dns_networks.unwrap();
885        let mainnet_dns: LinkEntry =
886            Chain::mainnet().public_dns_network_protocol().unwrap().parse().unwrap();
887        assert!(bootstrap_nodes.contains(&mainnet_dns));
888        assert_eq!(bootstrap_nodes.len(), 1);
889    }
890
891    #[test]
892    fn test_network_fork_filter_default() {
893        let mut chain_spec = Arc::clone(&MAINNET);
894
895        // remove any `next` fields we would have by removing all hardforks
896        Arc::make_mut(&mut chain_spec).hardforks = Default::default();
897
898        // check that the forkid is initialized with the genesis and no other forks
899        let genesis_fork_hash = ForkHash::from(chain_spec.genesis_hash());
900
901        // enforce that the fork_id set in the status is consistent with the generated fork filter
902        let config = builder().build_with_noop_provider(chain_spec);
903
904        let status = config.status;
905        let fork_filter = config.fork_filter;
906
907        // assert that there are no other forks
908        assert_eq!(status.forkid.next, 0);
909
910        // assert the same thing for the fork_filter
911        assert_eq!(fork_filter.current().next, 0);
912
913        // check status and fork_filter forkhash
914        assert_eq!(status.forkid.hash, genesis_fork_hash);
915        assert_eq!(fork_filter.current().hash, genesis_fork_hash);
916    }
917
918    #[test]
919    fn test_discv5_fork_id_default() {
920        const GENESIS_TIME: u64 = 151_515;
921
922        let genesis = Genesis::default().with_timestamp(GENESIS_TIME);
923
924        let active_fork = (EthereumHardfork::Shanghai, ForkCondition::Timestamp(GENESIS_TIME));
925        let future_fork = (EthereumHardfork::Cancun, ForkCondition::Timestamp(GENESIS_TIME + 1));
926
927        let chain_spec = ChainSpecBuilder::default()
928            .chain(Chain::dev())
929            .genesis(genesis)
930            .with_fork(active_fork.0, active_fork.1)
931            .with_fork(future_fork.0, future_fork.1)
932            .build();
933
934        // get the fork id to advertise on discv5
935        let genesis_fork_hash = ForkHash::from(chain_spec.genesis_hash());
936        let fork_id = ForkId { hash: genesis_fork_hash, next: GENESIS_TIME + 1 };
937        // check the fork id is set to active fork and _not_ yet future fork
938        assert_eq!(
939            fork_id,
940            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        assert_ne!(fork_id, chain_spec.latest_fork_id());
949
950        // enforce that the fork_id set in local enr
951        let fork_key = b"odyssey";
952        let config = builder()
953            .discovery_v5(
954                reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 30303).into())
955                    .fork(fork_key, fork_id),
956            )
957            .build_with_noop_provider(Arc::new(chain_spec));
958
959        let (local_enr, _, _, _) = build_local_enr(
960            &config.secret_key,
961            &config.discovery_v5_config.expect("should build config"),
962        );
963
964        // peers on the odyssey network will check discovered enrs for the 'odyssey' key and
965        // decide based on this if they attempt and rlpx connection to the peer or not
966        let advertised_fork_id = *local_enr
967            .get_decodable::<Vec<ForkId>>(fork_key)
968            .expect("should read 'odyssey'")
969            .expect("should decode fork id list")
970            .first()
971            .expect("should be non-empty");
972
973        assert_eq!(advertised_fork_id, fork_id);
974    }
975
976    #[test]
977    fn test_discv5_fork_id_custom_chain_id_fallback() {
978        const GENESIS_TIME: u64 = 151_515;
979
980        let genesis = Genesis::default().with_timestamp(GENESIS_TIME);
981
982        let chain_spec = ChainSpecBuilder::default()
983            .chain(Chain::from_id(3151908))
984            .genesis(genesis)
985            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(GENESIS_TIME))
986            .build();
987
988        let fork_id = chain_spec.fork_id(&Head {
989            hash: chain_spec.genesis_hash(),
990            number: 0,
991            timestamp: GENESIS_TIME,
992            difficulty: U256::ZERO,
993            total_difficulty: U256::ZERO,
994        });
995
996        let config = builder()
997            .discovery_v5(reth_discv5::Config::builder((Ipv4Addr::LOCALHOST, 30303).into()))
998            .build_with_noop_provider(Arc::new(chain_spec));
999
1000        let (local_enr, _, _, _) = build_local_enr(
1001            &config.secret_key,
1002            &config.discovery_v5_config.expect("should build config"),
1003        );
1004
1005        let advertised_fork_id = *local_enr
1006            .get_decodable::<Vec<ForkId>>(NetworkStackId::ETH)
1007            .expect("should read 'eth'")
1008            .expect("should decode fork id list")
1009            .first()
1010            .expect("should be non-empty");
1011
1012        assert_eq!(advertised_fork_id, fork_id);
1013    }
1014}