Skip to main content

reth_discv5/
config.rs

1//! Wrapper around [`discv5::Config`].
2
3use std::{
4    collections::HashSet,
5    fmt::Debug,
6    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
7};
8
9use alloy_primitives::Bytes;
10use derive_more::Display;
11use discv5::ListenConfig;
12use multiaddr::{Multiaddr, Protocol};
13use reth_ethereum_forks::{EnrForkIdEntry, ForkId};
14use reth_network_peers::NodeRecord;
15use tracing::debug;
16
17use crate::{enr::discv4_id_to_multiaddr_id, filter::MustNotIncludeKeys, NetworkStackId};
18
19/// The default address for discv5 via UDP is IPv4.
20///
21/// Default is 0.0.0.0, all interfaces. See [`discv5::ListenConfig`] default.
22pub const DEFAULT_DISCOVERY_V5_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
23
24/// The default IPv6 address for discv5 via UDP.
25///
26/// Default is ::, all interfaces.
27pub const DEFAULT_DISCOVERY_V5_ADDR_IPV6: Ipv6Addr = Ipv6Addr::UNSPECIFIED;
28
29/// The default port for discv5 via UDP.
30///
31/// Default is port 9200.
32pub const DEFAULT_DISCOVERY_V5_PORT: u16 = 9200;
33
34/// The default [`discv5::ListenConfig`].
35///
36/// This is different from the upstream default.
37pub const DEFAULT_DISCOVERY_V5_LISTEN_CONFIG: ListenConfig =
38    ListenConfig::Ipv4 { ip: DEFAULT_DISCOVERY_V5_ADDR, port: DEFAULT_DISCOVERY_V5_PORT };
39
40/// Default interval in seconds at which to run a lookup up query.
41///
42/// Default is 20 seconds.
43pub const DEFAULT_SECONDS_LOOKUP_INTERVAL: u64 = 20;
44
45/// Default number of times to do pulse lookup queries, at bootstrap (pulse intervals, defaulting
46/// to 5 seconds).
47///
48/// Default is 200 counts.
49pub const DEFAULT_COUNT_BOOTSTRAP_LOOKUPS: u64 = 200;
50
51/// Default duration of the pulse lookup interval at bootstrap.
52///
53/// Default is 5 seconds.
54pub const DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL: u64 = 5;
55
56/// Builds a [`Config`].
57#[derive(Debug)]
58pub struct ConfigBuilder {
59    /// Config used by [`discv5::Discv5`]. Contains the discovery listen socket.
60    discv5_config: Option<discv5::Config>,
61    /// Nodes to boot from.
62    bootstrap_nodes: HashSet<BootNode>,
63    /// Fork kv-pair to set in local node record. Identifies which network/chain/fork the node
64    /// belongs, e.g. `(b"opstack", ChainId)` or `(b"eth", ForkId)`.
65    ///
66    /// Defaults to L1 mainnet if not set.
67    fork: Option<(&'static [u8], ForkId)>,
68    /// `RLPx` TCP socket to advertise.
69    ///
70    /// NOTE: IP address of `RLPx` socket overwrites IP address of same IP version in
71    /// [`discv5::ListenConfig`].
72    tcp_socket: SocketAddr,
73    /// IPv4 address to advertise in the local ENR instead of the listen socket address.
74    ///
75    /// This is separate from [`discv5::ListenConfig`] because the listen address describes where
76    /// discv5 binds its UDP socket. Nodes commonly bind to an unspecified address like `0.0.0.0`
77    /// while advertising an externally reachable address from NAT configuration.
78    advertised_ipv4: Option<Ipv4Addr>,
79    /// IPv6 address to advertise in the local ENR instead of the listen socket address. See
80    /// [`Self::advertised_ipv4`].
81    advertised_ipv6: Option<Ipv6Addr>,
82    /// List of `(key, rlp-encoded-value)` tuples that should be advertised in local node record
83    /// (in addition to tcp port, udp port and fork).
84    other_enr_kv_pairs: Vec<(&'static [u8], Bytes)>,
85    /// Interval in seconds at which to run a lookup up query to populate kbuckets.
86    lookup_interval: Option<u64>,
87    /// Interval in seconds at which to run pulse lookup queries at bootstrap to boost kbucket
88    /// population.
89    bootstrap_lookup_interval: Option<u64>,
90    /// Number of times to run boost lookup queries at start up.
91    bootstrap_lookup_countdown: Option<u64>,
92    /// Custom filter rules to apply to a discovered peer in order to determine if it should be
93    /// passed up to rlpx or dropped.
94    discovered_peer_filter: Option<MustNotIncludeKeys>,
95}
96
97impl ConfigBuilder {
98    /// Returns a new builder, with all fields set like given instance.
99    pub fn new_from(discv5_config: Config) -> Self {
100        let Config {
101            discv5_config,
102            bootstrap_nodes,
103            fork,
104            tcp_socket,
105            advertised_ipv4,
106            advertised_ipv6,
107            other_enr_kv_pairs,
108            lookup_interval,
109            bootstrap_lookup_interval,
110            bootstrap_lookup_countdown,
111            discovered_peer_filter,
112        } = discv5_config;
113
114        Self {
115            discv5_config: Some(discv5_config),
116            bootstrap_nodes,
117            fork: fork.map(|(key, fork_id)| (key, fork_id.fork_id)),
118            tcp_socket,
119            advertised_ipv4,
120            advertised_ipv6,
121            other_enr_kv_pairs,
122            lookup_interval: Some(lookup_interval),
123            bootstrap_lookup_interval: Some(bootstrap_lookup_interval),
124            bootstrap_lookup_countdown: Some(bootstrap_lookup_countdown),
125            discovered_peer_filter: Some(discovered_peer_filter),
126        }
127    }
128
129    /// Set [`discv5::Config`], which contains the [`discv5::Discv5`] listen socket.
130    pub fn discv5_config(mut self, discv5_config: discv5::Config) -> Self {
131        self.discv5_config = Some(discv5_config);
132        self
133    }
134
135    /// Adds multiple boot nodes from a list of [`Enr`](discv5::Enr)s.
136    pub fn add_signed_boot_nodes(mut self, nodes: impl IntoIterator<Item = discv5::Enr>) -> Self {
137        self.bootstrap_nodes.extend(nodes.into_iter().map(BootNode::Enr));
138        self
139    }
140
141    /// Parses a comma-separated list of serialized [`Enr`](discv5::Enr)s, signed node records, and
142    /// adds any successfully deserialized records to boot nodes. Note: this type is serialized in
143    /// CL format since [`discv5`] is originally a CL library.
144    pub fn add_cl_serialized_signed_boot_nodes(mut self, enrs: &str) -> Self {
145        let bootstrap_nodes = &mut self.bootstrap_nodes;
146        for node in enrs.split(&[',']).flat_map(|record| record.trim().parse::<discv5::Enr>()) {
147            bootstrap_nodes.insert(BootNode::Enr(node));
148        }
149        self
150    }
151
152    /// Adds boot nodes in the form a list of [`NodeRecord`]s, parsed enodes.
153    pub fn add_unsigned_boot_nodes(mut self, enodes: impl IntoIterator<Item = NodeRecord>) -> Self {
154        for node in enodes {
155            if let Ok(node) = BootNode::from_unsigned(node) {
156                self.bootstrap_nodes.insert(node);
157            }
158        }
159
160        self
161    }
162
163    /// Adds a comma-separated list of enodes, serialized unsigned node records, to boot nodes.
164    pub fn add_serialized_unsigned_boot_nodes(mut self, enodes: &[&str]) -> Self {
165        for node in enodes {
166            if let Ok(node) = node.parse() &&
167                let Ok(node) = BootNode::from_unsigned(node)
168            {
169                self.bootstrap_nodes.insert(node);
170            }
171        }
172
173        self
174    }
175
176    /// Set fork ID kv-pair to set in local [`Enr`](enr::Enr). This lets peers on discovery
177    /// network know which chain this node belongs to.
178    pub const fn fork(mut self, fork_key: &'static [u8], fork_id: ForkId) -> Self {
179        self.fork = Some((fork_key, fork_id));
180        self
181    }
182
183    /// Sets the fork ID kv-pair if none has already been configured.
184    pub const fn fork_if_unset(mut self, fork_key: &'static [u8], fork_id: ForkId) -> Self {
185        if self.fork.is_none() {
186            self.fork = Some((fork_key, fork_id));
187        }
188        self
189    }
190
191    /// Sets the tcp socket to advertise in the local [`Enr`](enr::Enr). The IP address of
192    /// this socket will overwrite the discovery address of the same IP version, if one is
193    /// configured.
194    pub const fn tcp_socket(mut self, socket: SocketAddr) -> Self {
195        self.tcp_socket = socket;
196        self
197    }
198
199    /// Sets the IP address to advertise in the local [`Enr`](enr::Enr), without changing
200    /// the discv5 listen socket.
201    ///
202    /// Routed to the matching address family, so calling this once per family yields a dual-stack
203    /// advertisement.
204    pub const fn advertised_ip(mut self, ip: IpAddr) -> Self {
205        match ip {
206            IpAddr::V4(ip) => self.advertised_ipv4 = Some(ip),
207            IpAddr::V6(ip) => self.advertised_ipv6 = Some(ip),
208        }
209        self
210    }
211
212    /// Adds an additional kv-pair to include in the local [`Enr`](enr::Enr). Takes the key
213    /// to use for the kv-pair and the rlp encoded value.
214    pub fn add_enr_kv_pair(mut self, key: &'static [u8], value: Bytes) -> Self {
215        self.other_enr_kv_pairs.push((key, value));
216        self
217    }
218
219    /// Sets the interval at which to run lookup queries, in order to fill kbuckets. Lookup queries
220    /// are done periodically at the given interval for the whole run of the program.
221    pub const fn lookup_interval(mut self, seconds: u64) -> Self {
222        self.lookup_interval = Some(seconds);
223        self
224    }
225
226    /// Sets the interval at which to run boost lookup queries at start up. Queries will be started
227    /// at this interval for the configured number of times after start up.
228    pub const fn bootstrap_lookup_interval(mut self, seconds: u64) -> Self {
229        self.bootstrap_lookup_interval = Some(seconds);
230        self
231    }
232
233    /// Sets the number of times at which to run boost lookup queries to bootstrap the node.
234    pub const fn bootstrap_lookup_countdown(mut self, counts: u64) -> Self {
235        self.bootstrap_lookup_countdown = Some(counts);
236        self
237    }
238
239    /// Adds keys to disallow when filtering a discovered peer, to determine whether or not it
240    /// should be passed to rlpx. The discovered node record is scanned for any kv-pairs where the
241    /// key matches the disallowed keys. If not explicitly set, b"eth2" key will be disallowed.
242    pub fn must_not_include_keys(mut self, not_keys: &[&'static [u8]]) -> Self {
243        let mut filter = self.discovered_peer_filter.unwrap_or_default();
244        filter.add_disallowed_keys(not_keys);
245        self.discovered_peer_filter = Some(filter);
246        self
247    }
248
249    /// Returns a new [`Config`].
250    pub fn build(self) -> Config {
251        let Self {
252            discv5_config,
253            bootstrap_nodes,
254            fork,
255            tcp_socket,
256            advertised_ipv4,
257            advertised_ipv6,
258            other_enr_kv_pairs,
259            lookup_interval,
260            bootstrap_lookup_interval,
261            bootstrap_lookup_countdown,
262            discovered_peer_filter,
263        } = self;
264
265        let mut discv5_config = discv5_config.unwrap_or_else(|| {
266            discv5::ConfigBuilder::new(DEFAULT_DISCOVERY_V5_LISTEN_CONFIG).build()
267        });
268
269        discv5_config.listen_config =
270            amend_listen_config_wrt_rlpx(&discv5_config.listen_config, tcp_socket.ip());
271        if advertised_ipv4.is_some() || advertised_ipv6.is_some() {
272            // The upstream discv5 service can update local ENR IP fields from peer-observed
273            // socket addresses. When the operator configured a NAT address, that address is
274            // intentional advertisement state and must not be replaced by peer observations.
275            discv5_config.enr_update = false;
276        }
277
278        let fork = fork.map(|(key, fork_id)| (key, fork_id.into()));
279
280        let lookup_interval = lookup_interval.unwrap_or(DEFAULT_SECONDS_LOOKUP_INTERVAL);
281        let bootstrap_lookup_interval =
282            bootstrap_lookup_interval.unwrap_or(DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL);
283        let bootstrap_lookup_countdown =
284            bootstrap_lookup_countdown.unwrap_or(DEFAULT_COUNT_BOOTSTRAP_LOOKUPS);
285
286        let discovered_peer_filter = discovered_peer_filter
287            .unwrap_or_else(|| MustNotIncludeKeys::new(&[NetworkStackId::ETH2]));
288
289        Config {
290            discv5_config,
291            bootstrap_nodes,
292            fork,
293            tcp_socket,
294            advertised_ipv4,
295            advertised_ipv6,
296            other_enr_kv_pairs,
297            lookup_interval,
298            bootstrap_lookup_interval,
299            bootstrap_lookup_countdown,
300            discovered_peer_filter,
301        }
302    }
303}
304
305/// Config used to bootstrap [`discv5::Discv5`].
306#[derive(Clone, Debug)]
307pub struct Config {
308    /// Config used by [`discv5::Discv5`]. Contains the [`ListenConfig`], with the discovery listen
309    /// socket.
310    pub(super) discv5_config: discv5::Config,
311    /// Nodes to boot from.
312    pub(super) bootstrap_nodes: HashSet<BootNode>,
313    /// Fork kv-pair to set in local node record. Identifies which network/chain/fork the node
314    /// belongs, e.g. `(b"opstack", ChainId)` or `(b"eth", [ForkId])`.
315    pub(super) fork: Option<(&'static [u8], EnrForkIdEntry)>,
316    /// `RLPx` TCP socket to advertise.
317    ///
318    /// NOTE: IP address of `RLPx` socket overwrites IP address of same IP version in
319    /// [`discv5::ListenConfig`].
320    pub(super) tcp_socket: SocketAddr,
321    /// IPv4 address to advertise in the local ENR instead of the listen socket address.
322    pub(super) advertised_ipv4: Option<Ipv4Addr>,
323    /// IPv6 address to advertise in the local ENR instead of the listen socket address.
324    pub(super) advertised_ipv6: Option<Ipv6Addr>,
325    /// Additional kv-pairs (besides tcp port, udp port and fork) that should be advertised to
326    /// peers by including in local node record.
327    pub(super) other_enr_kv_pairs: Vec<(&'static [u8], Bytes)>,
328    /// Interval in seconds at which to run a lookup up query with to populate kbuckets.
329    pub(super) lookup_interval: u64,
330    /// Interval in seconds at which to run pulse lookup queries at bootstrap to boost kbucket
331    /// population.
332    pub(super) bootstrap_lookup_interval: u64,
333    /// Number of times to run boost lookup queries at start up.
334    pub(super) bootstrap_lookup_countdown: u64,
335    /// Custom filter rules to apply to a discovered peer in order to determine if it should be
336    /// passed up to rlpx or dropped.
337    pub(super) discovered_peer_filter: MustNotIncludeKeys,
338}
339
340impl Config {
341    /// Returns a new [`ConfigBuilder`], with the `RLPx` TCP port and IP version configured w.r.t.
342    /// the given socket.
343    pub fn builder(rlpx_tcp_socket: SocketAddr) -> ConfigBuilder {
344        ConfigBuilder {
345            discv5_config: None,
346            bootstrap_nodes: HashSet::default(),
347            fork: None,
348            tcp_socket: rlpx_tcp_socket,
349            advertised_ipv4: None,
350            advertised_ipv6: None,
351            other_enr_kv_pairs: Vec::new(),
352            lookup_interval: None,
353            bootstrap_lookup_interval: None,
354            bootstrap_lookup_countdown: None,
355            discovered_peer_filter: None,
356        }
357    }
358
359    /// Returns a mutable reference to the inner [`discv5::Config`]. This allows overriding
360    /// the listen config after the config has been built.
361    pub const fn discv5_config_mut(&mut self) -> &mut discv5::Config {
362        &mut self.discv5_config
363    }
364
365    /// Returns `true` if any socket in the discv5 listen config matches the given address.
366    pub fn has_matching_socket(&self, addr: SocketAddr) -> bool {
367        ipv4(&self.discv5_config.listen_config).is_some_and(|v4| SocketAddr::V4(v4) == addr) ||
368            ipv6(&self.discv5_config.listen_config).is_some_and(|v6| SocketAddr::V6(v6) == addr)
369    }
370
371    /// Inserts a new boot node to the list of boot nodes.
372    pub fn insert_boot_node(&mut self, boot_node: BootNode) {
373        self.bootstrap_nodes.insert(boot_node);
374    }
375
376    /// Inserts a new unsigned enode boot node to the list of boot nodes if it can be parsed, see
377    /// also [`BootNode::from_unsigned`].
378    pub fn insert_unsigned_boot_node(&mut self, node_record: NodeRecord) {
379        let _ = BootNode::from_unsigned(node_record).map(|node| self.insert_boot_node(node));
380    }
381
382    /// Extends the list of boot nodes with a list of enode boot nodes if they can be parsed.
383    pub fn extend_unsigned_boot_nodes(
384        &mut self,
385        node_records: impl IntoIterator<Item = NodeRecord>,
386    ) {
387        for node_record in node_records {
388            self.insert_unsigned_boot_node(node_record);
389        }
390    }
391
392    /// Returns the discovery (UDP) socket contained in the [`discv5::Config`]. Returns the IPv6
393    /// socket, if both IPv4 and v6 are configured. This socket will be advertised to peers in the
394    /// local [`Enr`](enr::Enr).
395    pub fn discovery_socket(&self) -> SocketAddr {
396        // Prefer v6 when both are configured (matches original `DualStack` behavior).
397        ipv6(&self.discv5_config.listen_config)
398            .map(SocketAddr::V6)
399            .or_else(|| ipv4(&self.discv5_config.listen_config).map(SocketAddr::V4))
400            .unwrap_or_else(|| SocketAddr::from((std::net::Ipv4Addr::UNSPECIFIED, 0)))
401    }
402
403    /// Returns the `RLPx` (TCP) socket contained in the [`discv5::Config`]. This socket will be
404    /// advertised to peers in the local [`Enr`](enr::Enr).
405    pub const fn rlpx_socket(&self) -> &SocketAddr {
406        &self.tcp_socket
407    }
408
409    /// Sets the port of the `RLPx` TCP socket to advertise. This allows advertising the actually
410    /// bound listener port when the configured port was 0 (OS-assigned).
411    pub const fn set_rlpx_port(&mut self, port: u16) {
412        self.tcp_socket.set_port(port);
413    }
414}
415
416/// Returns the IPv4 discovery socket if one is configured.
417pub fn ipv4(listen_config: &ListenConfig) -> Option<SocketAddrV4> {
418    match listen_config {
419        ListenConfig::Ipv4 { ip, port } |
420        ListenConfig::DualStack { ipv4: ip, ipv4_port: port, .. } => {
421            Some(SocketAddrV4::new(*ip, *port))
422        }
423        ListenConfig::FromSockets { ipv4: Some(s), .. } => match s.local_addr().ok()? {
424            SocketAddr::V4(addr) => Some(addr),
425            SocketAddr::V6(_) => None,
426        },
427        _ => None,
428    }
429}
430
431/// Returns the IPv6 discovery socket if one is configured.
432pub fn ipv6(listen_config: &ListenConfig) -> Option<SocketAddrV6> {
433    match listen_config {
434        ListenConfig::Ipv6 { ip, port } |
435        ListenConfig::DualStack { ipv6: ip, ipv6_port: port, .. } => {
436            Some(SocketAddrV6::new(*ip, *port, 0, 0))
437        }
438        ListenConfig::FromSockets { ipv6: Some(s), .. } => match s.local_addr().ok()? {
439            SocketAddr::V6(addr) => Some(addr),
440            SocketAddr::V4(_) => None,
441        },
442        _ => None,
443    }
444}
445
446/// Returns the address for the opposite-family sibling socket in shared-port mode: that
447/// family's configured listen address, rebound to the shared port that discv4 advertises
448/// for both families.
449pub fn sibling_addr(
450    listen_config: &ListenConfig,
451    primary: SocketAddr,
452    shared_port: u16,
453) -> Option<SocketAddr> {
454    let mut addr = if primary.is_ipv4() {
455        SocketAddr::V6(ipv6(listen_config)?)
456    } else {
457        SocketAddr::V4(ipv4(listen_config)?)
458    };
459    addr.set_port(shared_port);
460    Some(addr)
461}
462
463/// Returns the amended [`discv5::ListenConfig`] based on the `RLPx` IP address. The ENR is limited
464/// to one IP address per IP version (atm, may become spec'd how to advertise different addresses).
465/// The `RLPx` address overwrites the discv5 address w.r.t. IP version.
466pub fn amend_listen_config_wrt_rlpx(
467    listen_config: &ListenConfig,
468    rlpx_addr: IpAddr,
469) -> ListenConfig {
470    let discv5_socket_ipv4 = ipv4(listen_config);
471    let discv5_socket_ipv6 = ipv6(listen_config);
472
473    let discv5_port_ipv4 =
474        discv5_socket_ipv4.map(|socket| socket.port()).unwrap_or(DEFAULT_DISCOVERY_V5_PORT);
475    let discv5_addr_ipv4 = discv5_socket_ipv4.map(|socket| *socket.ip());
476    let discv5_port_ipv6 =
477        discv5_socket_ipv6.map(|socket| socket.port()).unwrap_or(DEFAULT_DISCOVERY_V5_PORT);
478    let discv5_addr_ipv6 = discv5_socket_ipv6.map(|socket| *socket.ip());
479
480    let (discv5_socket_ipv4, discv5_socket_ipv6) = discv5_sockets_wrt_rlpx_addr(
481        rlpx_addr,
482        discv5_addr_ipv4,
483        discv5_port_ipv4,
484        discv5_addr_ipv6,
485        discv5_port_ipv6,
486    );
487
488    ListenConfig::from_two_sockets(discv5_socket_ipv4, discv5_socket_ipv6)
489}
490
491/// Returns the sockets that can be used for discv5 with respect to the `RLPx` address. ENR specs
492/// only acknowledge one address per IP version.
493pub fn discv5_sockets_wrt_rlpx_addr(
494    rlpx_addr: IpAddr,
495    discv5_addr_ipv4: Option<Ipv4Addr>,
496    discv5_port_ipv4: u16,
497    discv5_addr_ipv6: Option<Ipv6Addr>,
498    discv5_port_ipv6: u16,
499) -> (Option<SocketAddrV4>, Option<SocketAddrV6>) {
500    match rlpx_addr {
501        IpAddr::V4(rlpx_addr) => {
502            let discv5_socket_ipv6 =
503                discv5_addr_ipv6.map(|ip| SocketAddrV6::new(ip, discv5_port_ipv6, 0, 0));
504
505            if let Some(discv5_addr) = discv5_addr_ipv4 &&
506                discv5_addr != rlpx_addr
507            {
508                debug!(target: "net::discv5",
509                    %discv5_addr,
510                    %rlpx_addr,
511                    "Overwriting discv5 IPv4 address with RLPx IPv4 address, limited to one advertised IP address per IP version"
512                );
513            }
514
515            // overwrite discv5 ipv4 addr with RLPx address. this is since there is no
516            // spec'd way to advertise a different address for rlpx and discovery in the
517            // ENR.
518            (Some(SocketAddrV4::new(rlpx_addr, discv5_port_ipv4)), discv5_socket_ipv6)
519        }
520        IpAddr::V6(rlpx_addr) => {
521            let discv5_socket_ipv4 =
522                discv5_addr_ipv4.map(|ip| SocketAddrV4::new(ip, discv5_port_ipv4));
523
524            if let Some(discv5_addr) = discv5_addr_ipv6 &&
525                discv5_addr != rlpx_addr
526            {
527                debug!(target: "net::discv5",
528                    %discv5_addr,
529                    %rlpx_addr,
530                    "Overwriting discv5 IPv6 address with RLPx IPv6 address, limited to one advertised IP address per IP version"
531                );
532            }
533
534            // overwrite discv5 ipv6 addr with RLPx address. this is since there is no
535            // spec'd way to advertise a different address for rlpx and discovery in the
536            // ENR.
537            (discv5_socket_ipv4, Some(SocketAddrV6::new(rlpx_addr, discv5_port_ipv6, 0, 0)))
538        }
539    }
540}
541
542/// A boot node can be added either as a string in either 'enode' URL scheme or serialized from
543/// [`Enr`](discv5::Enr) type.
544#[derive(Clone, Debug, PartialEq, Eq, Hash, Display)]
545pub enum BootNode {
546    /// An unsigned node record.
547    #[display("{_0}")]
548    Enode(Multiaddr),
549    /// A signed node record.
550    #[display("{_0:?}")]
551    Enr(discv5::Enr),
552}
553
554impl BootNode {
555    /// Parses a [`NodeRecord`] and serializes according to CL format. Note: [`discv5`] is
556    /// originally a CL library hence needs this format to add the node.
557    pub fn from_unsigned(node_record: NodeRecord) -> Result<Self, secp256k1::Error> {
558        let NodeRecord { address, udp_port, id, .. } = node_record;
559        let mut multi_address = Multiaddr::empty();
560        match address {
561            IpAddr::V4(ip) => multi_address.push(Protocol::Ip4(ip)),
562            IpAddr::V6(ip) => multi_address.push(Protocol::Ip6(ip)),
563        }
564
565        multi_address.push(Protocol::Udp(udp_port));
566        let id = discv4_id_to_multiaddr_id(id)?;
567        multi_address.push(Protocol::P2p(id));
568
569        Ok(Self::Enode(multi_address))
570    }
571}
572
573#[cfg(test)]
574mod test {
575    use super::*;
576    use alloy_primitives::hex;
577    use std::net::SocketAddrV4;
578
579    const MULTI_ADDRESSES: &str = "/ip4/184.72.129.189/udp/30301/p2p/16Uiu2HAmSG2hdLwyQHQmG4bcJBgD64xnW63WMTLcrNq6KoZREfGb,/ip4/3.231.11.52/udp/30301/p2p/16Uiu2HAmMy4V8bi3XP7KDfSLQcLACSvTLroRRwEsTyFUKo8NCkkp,/ip4/54.198.153.150/udp/30301/p2p/16Uiu2HAmSVsb7MbRf1jg3Dvd6a3n5YNqKQwn1fqHCFgnbqCsFZKe,/ip4/3.220.145.177/udp/30301/p2p/16Uiu2HAm74pBDGdQ84XCZK27GRQbGFFwQ7RsSqsPwcGmCR3Cwn3B,/ip4/3.231.138.188/udp/30301/p2p/16Uiu2HAmMnTiJwgFtSVGV14ZNpwAvS1LUoF4pWWeNtURuV6C3zYB";
580    const BOOT_NODES_OP_MAINNET_AND_BASE_MAINNET: &[&str] = &[
581        "enode://ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec@34.65.175.185:30305",
582        "enode://dd751a9ef8912be1bfa7a5e34e2c3785cc5253110bd929f385e07ba7ac19929fb0e0c5d93f77827291f4da02b2232240fbc47ea7ce04c46e333e452f8656b667@34.65.107.0:30305",
583        "enode://c5d289b56a77b6a2342ca29956dfd07aadf45364dde8ab20d1dc4efd4d1bc6b4655d902501daea308f4d8950737a4e93a4dfedd17b49cd5760ffd127837ca965@34.65.202.239:30305",
584        "enode://87a32fd13bd596b2ffca97020e31aef4ddcc1bbd4b95bb633d16c1329f654f34049ed240a36b449fda5e5225d70fe40bc667f53c304b71f8e68fc9d448690b51@3.231.138.188:30301",
585        "enode://ca21ea8f176adb2e229ce2d700830c844af0ea941a1d8152a9513b966fe525e809c3a6c73a2c18a12b74ed6ec4380edf91662778fe0b79f6a591236e49e176f9@184.72.129.189:30301",
586        "enode://acf4507a211ba7c1e52cdf4eef62cdc3c32e7c9c47998954f7ba024026f9a6b2150cd3f0b734d9c78e507ab70d59ba61dfe5c45e1078c7ad0775fb251d7735a2@3.220.145.177:30301",
587        "enode://8a5a5006159bf079d06a04e5eceab2a1ce6e0f721875b2a9c96905336219dbe14203d38f70f3754686a6324f786c2f9852d8c0dd3adac2d080f4db35efc678c5@3.231.11.52:30301",
588        "enode://cdadbe835308ad3557f9a1de8db411da1a260a98f8421d62da90e71da66e55e98aaa8e90aa7ce01b408a54e4bd2253d701218081ded3dbe5efbbc7b41d7cef79@54.198.153.150:30301",
589    ];
590
591    #[test]
592    fn parse_boot_nodes() {
593        const OP_SEPOLIA_CL_BOOTNODES: &str = "enr:-J64QBwRIWAco7lv6jImSOjPU_W266lHXzpAS5YOh7WmgTyBZkgLgOwo_mxKJq3wz2XRbsoBItbv1dCyjIoNq67mFguGAYrTxM42gmlkgnY0gmlwhBLSsHKHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDmoWSi8hcsRpQf2eJsNUx-sqv6fH4btmo2HsAzZFAKnKDdGNwgiQGg3VkcIIkBg,enr:-J64QFa3qMsONLGphfjEkeYyF6Jkil_jCuJmm7_a42ckZeUQGLVzrzstZNb1dgBp1GGx9bzImq5VxJLP-BaptZThGiWGAYrTytOvgmlkgnY0gmlwhGsV-zeHb3BzdGFja4S0lAUAiXNlY3AyNTZrMaEDahfSECTIS_cXyZ8IyNf4leANlZnrsMEWTkEYxf4GMCmDdGNwgiQGg3VkcIIkBg";
594
595        let config = Config::builder((Ipv4Addr::UNSPECIFIED, 30303).into())
596            .add_cl_serialized_signed_boot_nodes(OP_SEPOLIA_CL_BOOTNODES)
597            .build();
598
599        let socket_1 = "18.210.176.114:9222".parse::<SocketAddrV4>().unwrap();
600        let socket_2 = "107.21.251.55:9222".parse::<SocketAddrV4>().unwrap();
601
602        for node in config.bootstrap_nodes {
603            let BootNode::Enr(node) = node else { panic!() };
604            assert!(
605                socket_1 == node.udp4_socket().unwrap() && socket_1 == node.tcp4_socket().unwrap() ||
606                    socket_2 == node.udp4_socket().unwrap() &&
607                        socket_2 == node.tcp4_socket().unwrap()
608            );
609            assert_eq!("84b4940500", hex::encode(node.get_raw_rlp("opstack").unwrap()));
610        }
611    }
612
613    #[test]
614    fn parse_enodes() {
615        let config = Config::builder((Ipv4Addr::UNSPECIFIED, 30303).into())
616            .add_serialized_unsigned_boot_nodes(BOOT_NODES_OP_MAINNET_AND_BASE_MAINNET)
617            .build();
618
619        let bootstrap_nodes =
620            config.bootstrap_nodes.into_iter().map(|node| format!("{node}")).collect::<Vec<_>>();
621
622        for node in MULTI_ADDRESSES.split(&[',']) {
623            assert!(bootstrap_nodes.contains(&node.to_string()));
624        }
625    }
626
627    #[test]
628    fn overwrite_ipv4_addr() {
629        let rlpx_addr: Ipv4Addr = "192.168.0.1".parse().unwrap();
630
631        let listen_config = DEFAULT_DISCOVERY_V5_LISTEN_CONFIG;
632
633        let amended_config = amend_listen_config_wrt_rlpx(&listen_config, rlpx_addr.into());
634
635        let config_socket_ipv4 = ipv4(&amended_config).unwrap();
636
637        assert_eq!(*config_socket_ipv4.ip(), rlpx_addr);
638        assert_eq!(config_socket_ipv4.port(), DEFAULT_DISCOVERY_V5_PORT);
639        assert_eq!(ipv6(&amended_config), ipv6(&listen_config));
640    }
641
642    #[test]
643    fn overwrite_ipv6_addr() {
644        let rlpx_addr: Ipv6Addr = "fe80::1".parse().unwrap();
645
646        let listen_config = DEFAULT_DISCOVERY_V5_LISTEN_CONFIG;
647
648        let amended_config = amend_listen_config_wrt_rlpx(&listen_config, rlpx_addr.into());
649
650        let config_socket_ipv6 = ipv6(&amended_config).unwrap();
651
652        assert_eq!(*config_socket_ipv6.ip(), rlpx_addr);
653        assert_eq!(config_socket_ipv6.port(), DEFAULT_DISCOVERY_V5_PORT);
654        assert_eq!(ipv4(&amended_config), ipv4(&listen_config));
655    }
656
657    #[test]
658    fn sibling_addr_uses_shared_port() {
659        let listen_config = ListenConfig::DualStack {
660            ipv4: Ipv4Addr::UNSPECIFIED,
661            ipv4_port: 30303,
662            ipv6: Ipv6Addr::UNSPECIFIED,
663            ipv6_port: 9200,
664        };
665        let primary: SocketAddr = "0.0.0.0:30303".parse().unwrap();
666
667        let addr = sibling_addr(&listen_config, primary, 30303).unwrap();
668        assert_eq!(addr, "[::]:30303".parse::<SocketAddr>().unwrap());
669
670        let listen_config = ListenConfig::Ipv4 { ip: Ipv4Addr::UNSPECIFIED, port: 30303 };
671        assert_eq!(sibling_addr(&listen_config, primary, 30303), None);
672    }
673}