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