1use 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
19pub const DEFAULT_DISCOVERY_V5_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
23
24pub const DEFAULT_DISCOVERY_V5_ADDR_IPV6: Ipv6Addr = Ipv6Addr::UNSPECIFIED;
28
29pub const DEFAULT_DISCOVERY_V5_PORT: u16 = 9200;
33
34pub const DEFAULT_DISCOVERY_V5_LISTEN_CONFIG: ListenConfig =
38 ListenConfig::Ipv4 { ip: DEFAULT_DISCOVERY_V5_ADDR, port: DEFAULT_DISCOVERY_V5_PORT };
39
40pub const DEFAULT_SECONDS_LOOKUP_INTERVAL: u64 = 20;
44
45pub const DEFAULT_COUNT_BOOTSTRAP_LOOKUPS: u64 = 200;
50
51pub const DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL: u64 = 5;
55
56#[derive(Debug)]
58pub struct ConfigBuilder {
59 discv5_config: Option<discv5::Config>,
61 bootstrap_nodes: HashSet<BootNode>,
63 fork: Option<(&'static [u8], ForkId)>,
68 tcp_socket: SocketAddr,
73 advertised_ipv4: Option<Ipv4Addr>,
79 advertised_ipv6: Option<Ipv6Addr>,
82 other_enr_kv_pairs: Vec<(&'static [u8], Bytes)>,
85 lookup_interval: Option<u64>,
87 bootstrap_lookup_interval: Option<u64>,
90 bootstrap_lookup_countdown: Option<u64>,
92 discovered_peer_filter: Option<MustNotIncludeKeys>,
95}
96
97impl ConfigBuilder {
98 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 pub fn discv5_config(mut self, discv5_config: discv5::Config) -> Self {
131 self.discv5_config = Some(discv5_config);
132 self
133 }
134
135 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 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 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 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 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 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 pub const fn tcp_socket(mut self, socket: SocketAddr) -> Self {
195 self.tcp_socket = socket;
196 self
197 }
198
199 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 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 pub const fn lookup_interval(mut self, seconds: u64) -> Self {
222 self.lookup_interval = Some(seconds);
223 self
224 }
225
226 pub const fn bootstrap_lookup_interval(mut self, seconds: u64) -> Self {
229 self.bootstrap_lookup_interval = Some(seconds);
230 self
231 }
232
233 pub const fn bootstrap_lookup_countdown(mut self, counts: u64) -> Self {
235 self.bootstrap_lookup_countdown = Some(counts);
236 self
237 }
238
239 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 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 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#[derive(Clone, Debug)]
307pub struct Config {
308 pub(super) discv5_config: discv5::Config,
311 pub(super) bootstrap_nodes: HashSet<BootNode>,
313 pub(super) fork: Option<(&'static [u8], EnrForkIdEntry)>,
316 pub(super) tcp_socket: SocketAddr,
321 pub(super) advertised_ipv4: Option<Ipv4Addr>,
323 pub(super) advertised_ipv6: Option<Ipv6Addr>,
325 pub(super) other_enr_kv_pairs: Vec<(&'static [u8], Bytes)>,
328 pub(super) lookup_interval: u64,
330 pub(super) bootstrap_lookup_interval: u64,
333 pub(super) bootstrap_lookup_countdown: u64,
335 pub(super) discovered_peer_filter: MustNotIncludeKeys,
338}
339
340impl Config {
341 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 pub const fn discv5_config_mut(&mut self) -> &mut discv5::Config {
362 &mut self.discv5_config
363 }
364
365 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 pub fn insert_boot_node(&mut self, boot_node: BootNode) {
373 self.bootstrap_nodes.insert(boot_node);
374 }
375
376 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 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 pub fn discovery_socket(&self) -> SocketAddr {
396 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 pub const fn rlpx_socket(&self) -> &SocketAddr {
406 &self.tcp_socket
407 }
408
409 pub const fn set_rlpx_port(&mut self, port: u16) {
412 self.tcp_socket.set_port(port);
413 }
414}
415
416pub 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
431pub 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
446pub 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
463pub 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
491pub 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 (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 (discv5_socket_ipv4, Some(SocketAddrV6::new(rlpx_addr, discv5_port_ipv6, 0, 0)))
538 }
539 }
540}
541
542#[derive(Clone, Debug, PartialEq, Eq, Hash, Display)]
545pub enum BootNode {
546 #[display("{_0}")]
548 Enode(Multiaddr),
549 #[display("{_0:?}")]
551 Enr(discv5::Enr),
552}
553
554impl BootNode {
555 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}