1use clap::Parser;
4use reth_cli_util::{get_secret_key, load_secret_key::rng_secret_key};
5use reth_discv4::{DiscoveryUpdate, Discv4, Discv4Config};
6use reth_discv5::{
7 discv5::{self, Event, ListenConfig},
8 enr::EnrCombinedKeyWrapper,
9 Config, Discv5, IpMode,
10};
11use reth_net_nat::NatResolver;
12use reth_network_peers::{id2pk, pk2id, AnyNode, Enr, NodeRecord};
13use secp256k1::SecretKey;
14use socket2::{Domain, Protocol, Socket, Type};
15use std::{
16 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
17 path::PathBuf,
18 sync::Arc,
19};
20use tokio::{net::UdpSocket, select};
21use tokio_stream::StreamExt;
22use tracing::{info, warn};
23
24#[derive(Parser, Debug)]
26pub struct Command {
27 #[arg(long, default_value = "0.0.0.0:30301")]
29 pub addr: SocketAddr,
30
31 #[arg(long, value_name = "PATH")]
38 pub p2p_secret_key: Option<PathBuf>,
39
40 #[arg(long, default_value = "any")]
46 pub nat: Vec<NatResolver>,
47
48 #[arg(long)]
53 pub v5: bool,
54
55 #[arg(long, value_delimiter = ',')]
58 pub bootnodes: Vec<AnyNode>,
59}
60
61impl Command {
62 pub async fn execute(self) -> eyre::Result<()> {
64 info!("Bootnode started with config: {self:?}");
65
66 let sk = self.network_secret()?;
67 let local_enr = NodeRecord::from_secret_key(self.addr, &sk).with_tcp_port(0);
70 let nat = self.resolved_nat()?;
71 let seeds = self.seed_nodes(&nat)?;
72
73 let discv4_config = self.discv4_config(&nat, &seeds);
74
75 let (_discv4, mut discv4_service, _discv5, mut discv5_forwarder) = if self.v5 {
79 let shared_socket = bind_socket(self.addr).await?;
80 let shared_port = shared_socket.local_addr()?.port();
82
83 let mut discv5_config = self.discv5_config(&nat, &seeds);
87 let discv5_cfg = discv5_config.discv5_config_mut();
88 let secondary_addr = reth_discv5::config::sibling_addr(
89 &discv5_cfg.listen_config,
90 self.addr,
91 shared_port,
92 );
93 let secondary_socket = match secondary_addr {
94 Some(addr) => Some(bind_socket(addr).await?),
95 None => None,
96 };
97 let (ipv4, ipv6) = if self.addr.is_ipv4() {
98 (Some(shared_socket.clone()), secondary_socket.clone())
99 } else {
100 (secondary_socket.clone(), Some(shared_socket.clone()))
101 };
102 discv5_cfg.listen_config = ListenConfig::FromSockets { ipv4, ipv6 };
103
104 let (discv4, discv4_service, mut ingress) =
105 Discv4::bind_shared(shared_socket, secondary_socket, local_enr, sk, discv4_config)?;
106 info!("Started discv4 (shared port) at address: {local_enr:?}");
107
108 info!("Starting discv5 (shared port)");
109 let (discv5, mut updates) = Discv5::start(&sk, discv5_config).await?;
110 log_discv5_enr(&discv5);
111
112 let forwarder = tokio::spawn(async move {
114 while let Some(event) = updates.recv().await {
115 match event {
116 Event::UnrecognizedFrame(frame) => {
117 ingress.handle_packet(&frame.packet, frame.src_address).await
118 }
119 Event::SessionEstablished(enr, _) => {
120 info!("(Discv5) new peer added, peer_id={:?}", enr.id())
121 }
122 _ => {}
123 }
124 }
125 info!("(Discv5) update stream ended.");
126 });
127
128 (discv4, discv4_service, Some(discv5), Some(forwarder))
129 } else {
130 let (discv4, discv4_service) =
131 Discv4::bind(self.addr, local_enr, sk, discv4_config).await?;
132 info!("Started discv4 at address: {local_enr:?}");
133 (discv4, discv4_service, None, None)
134 };
135
136 let mut discv4_updates = discv4_service.update_stream();
137 discv4_service.spawn();
138
139 loop {
141 select! {
142 update = discv4_updates.next() => {
143 if let Some(update) = update {
144 match update {
145 DiscoveryUpdate::Added(record) => {
146 info!("(Discv4) new peer added, peer_id={:?}", record.id);
147 }
148 DiscoveryUpdate::Removed(peer_id) => {
149 info!("(Discv4) peer with peer-id={:?} removed", peer_id);
150 }
151 _ => {}
152 }
153 } else {
154 info!("(Discv4) update stream ended.");
155 break;
156 }
157 }
158 _ = async {
160 if let Some(forwarder) = &mut discv5_forwarder {
161 let _ = forwarder.await;
162 } else {
163 futures::future::pending::<()>().await
164 }
165 } => break,
166 }
167 }
168
169 Ok(())
170 }
171
172 fn network_secret(&self) -> eyre::Result<SecretKey> {
173 match &self.p2p_secret_key {
174 Some(path) => Ok(get_secret_key(path)?),
175 None => Ok(rng_secret_key()),
176 }
177 }
178
179 fn resolved_nat(&self) -> eyre::Result<BootnodeNat> {
186 match self.nat.as_slice() {
187 [] => Ok(BootnodeNat::single(NatResolver::Any, self.addr.port())),
188 [nat] => {
189 let mut single = BootnodeNat::single(nat.clone(), self.addr.port());
190 if let [ip] = single.advertised_ips[..] &&
195 ip.is_ipv4() != self.addr.is_ipv4()
196 {
197 single.resolver = NatResolver::None;
198 }
199 Ok(single)
200 }
201 [first, second] => {
202 let first_ip = fixed_external_ip(first)?;
203 let second_ip = fixed_external_ip(second)?;
204
205 if first_ip.is_ipv4() == second_ip.is_ipv4() {
206 eyre::bail!("repeated --nat requires one IPv4 and one IPv6 extip:<IP> value");
207 }
208
209 let primary_ip =
211 if first_ip.is_ipv4() == self.addr.is_ipv4() { first_ip } else { second_ip };
212
213 Ok(BootnodeNat {
214 resolver: NatResolver::ExternalIp(primary_ip),
215 advertised_ips: vec![first_ip, second_ip],
216 })
217 }
218 _ => {
219 eyre::bail!("--nat can be provided at most twice");
220 }
221 }
222 }
223
224 fn discv4_config(&self, nat: &BootnodeNat, seeds: &BootnodeSeeds) -> Discv4Config {
225 let mut builder = Discv4Config::builder();
226 builder.external_ip_resolver(Some(nat.resolver.clone())).add_boot_nodes(
227 seeds.discv4_records().filter(|record| {
228 let served = self.serves_family(record.address, nat);
229 if !served {
230 warn!(
231 "No socket will be bound for the IP family of --bootnodes entry {record}; \
232 it will not be contacted (advertise a matching --nat extip with --v5)"
233 );
234 }
235 served
236 }),
237 );
238 if self.v5 &&
240 let Some(ip) =
241 nat.advertised_ips.iter().find(|ip| ip.is_ipv4() != self.addr.is_ipv4())
242 {
243 builder.secondary_advertised_ip(*ip);
244 }
245 builder.build()
246 }
247
248 fn seed_nodes(&self, nat: &BootnodeNat) -> eyre::Result<BootnodeSeeds> {
251 let mut seeds = BootnodeSeeds::default();
252 for node in &self.bootnodes {
253 let record = match node {
254 AnyNode::NodeRecord(record) => {
255 let record = record.into_ipv4_mapped();
257 id2pk(record.id).map_err(|err| {
258 eyre::eyre!("--bootnodes entry has an invalid node id ({err}): {node}")
259 })?;
260 seeds.enodes.push(record);
261 record
262 }
263 AnyNode::Enr(enr) => {
264 let record = self.enr_endpoint(enr, nat)?;
265 seeds.enr_records.push(record);
266 seeds.enrs.push(EnrCombinedKeyWrapper::from(enr.clone()).0);
267 record
268 }
269 node => eyre::bail!(
270 "--bootnodes entries must be enode URLs with an IP host or signed ENRs: {node}"
271 ),
272 };
273 if record.udp_port == 0 {
274 eyre::bail!("--bootnodes entry has no discovery port: {node}");
275 }
276 }
277 Ok(seeds)
278 }
279
280 fn enr_endpoint(&self, enr: &Enr<SecretKey>, nat: &BootnodeNat) -> eyre::Result<NodeRecord> {
289 let id = pk2id(&enr.public_key());
290 let v4 = enr.ip4().zip(enr.udp4()).map(|(ip, udp_port)| NodeRecord {
291 address: ip.into(),
292 udp_port,
293 tcp_port: enr.tcp4().unwrap_or(0),
294 id,
295 });
296 let v6 = enr.ip6().zip(enr.udp6().or_else(|| enr.udp4())).map(|(ip, udp_port)| {
299 NodeRecord {
300 address: ip.into(),
301 udp_port,
302 tcp_port: enr.tcp6().or_else(|| enr.tcp4()).unwrap_or(0),
303 id,
304 }
305 .into_ipv4_mapped()
306 });
307
308 let nonzero = |record: &NodeRecord| record.udp_port != 0;
311 v4.into_iter()
312 .chain(v6)
313 .filter(nonzero)
314 .find(|record| self.serves_family(record.address, nat))
315 .or_else(|| v4.filter(nonzero))
316 .or_else(|| v6.filter(nonzero))
317 .or(v4)
318 .or(v6)
319 .ok_or_else(|| {
320 eyre::eyre!("unusable --bootnodes ENR (no ip+udp endpoint for one family): {enr}")
321 })
322 }
323
324 fn serves_family(&self, ip: IpAddr, nat: &BootnodeNat) -> bool {
327 ip.is_ipv4() == self.addr.is_ipv4() ||
328 (self.v5 && nat.advertised_ips.iter().any(|a| a.is_ipv4() == ip.is_ipv4()))
329 }
330
331 fn discv5_config(&self, nat: &BootnodeNat, seeds: &BootnodeSeeds) -> Config {
332 let mut builder = Config::builder(SocketAddr::new(self.addr.ip(), 0));
335
336 let port = self.addr.port();
338
339 let bind_ipv4 = self.addr.is_ipv4() || nat.advertised_ips.iter().any(IpAddr::is_ipv4);
343 let bind_ipv6 = self.addr.is_ipv6() || nat.advertised_ips.iter().any(IpAddr::is_ipv6);
344 let listen = if bind_ipv4 && bind_ipv6 {
345 ListenConfig::DualStack {
346 ipv4: Ipv4Addr::UNSPECIFIED,
347 ipv4_port: port,
348 ipv6: Ipv6Addr::UNSPECIFIED,
349 ipv6_port: port,
350 }
351 } else if bind_ipv6 {
352 ListenConfig::Ipv6 { ip: Ipv6Addr::UNSPECIFIED, port }
353 } else {
354 ListenConfig::Ipv4 { ip: Ipv4Addr::UNSPECIFIED, port }
355 };
356 builder = builder.discv5_config(discv5::ConfigBuilder::new(listen).build());
357
358 for ip in &nat.advertised_ips {
359 builder = builder.advertised_ip(*ip);
360 }
361
362 let enrs = seeds.enrs.iter().filter(|enr| {
365 let contactable = discv5_contactable(enr, bind_ipv4, bind_ipv6);
366 if !contactable {
367 warn!("--bootnodes ENR has no endpoint for a served IP family; discv5 will not dial it: {enr}");
368 }
369 contactable
370 });
371 let enodes = seeds.enodes.iter().filter(|record| {
372 if record.address.is_ipv4() {
373 bind_ipv4
374 } else {
375 bind_ipv6
376 }
377 });
378
379 builder =
380 builder.add_signed_boot_nodes(enrs.cloned()).add_unsigned_boot_nodes(enodes.copied());
381
382 builder.build()
383 }
384}
385
386#[derive(Debug)]
388struct BootnodeNat {
389 resolver: NatResolver,
391 advertised_ips: Vec<IpAddr>,
393}
394
395impl BootnodeNat {
396 fn single(resolver: NatResolver, port: u16) -> Self {
397 let advertised_ips = resolver.clone().as_external_ip(port).into_iter().collect();
398 Self { resolver, advertised_ips }
399 }
400}
401
402#[derive(Debug, Default)]
404struct BootnodeSeeds {
405 enodes: Vec<NodeRecord>,
407 enrs: Vec<discv5::Enr>,
409 enr_records: Vec<NodeRecord>,
411}
412
413impl BootnodeSeeds {
414 fn discv4_records(&self) -> impl Iterator<Item = NodeRecord> + '_ {
416 self.enodes.iter().chain(&self.enr_records).copied()
417 }
418}
419
420async fn bind_socket(addr: SocketAddr) -> eyre::Result<Arc<UdpSocket>> {
425 let socket = match addr {
426 SocketAddr::V4(_) => UdpSocket::bind(addr).await?,
427 SocketAddr::V6(_) => {
428 let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
429 socket.set_only_v6(true)?;
430 socket.set_nonblocking(true)?;
431 socket.bind(&addr.into())?;
432 UdpSocket::from_std(socket.into())?
433 }
434 };
435 Ok(Arc::new(socket))
436}
437
438fn fixed_external_ip(nat: &NatResolver) -> eyre::Result<IpAddr> {
439 match nat {
440 NatResolver::ExternalIp(ip) => Ok(*ip),
441 _ => {
442 eyre::bail!("--nat can only be repeated with extip:<IP> values");
443 }
444 }
445}
446
447fn discv5_contactable(enr: &discv5::Enr, bind_ipv4: bool, bind_ipv6: bool) -> bool {
451 match (bind_ipv4, bind_ipv6) {
452 (true, true) => IpMode::DualStack,
453 (false, true) => IpMode::Ip6,
454 _ => IpMode::Ip4,
455 }
456 .get_contactable_addr(enr)
457 .is_some_and(|addr| addr.port() != 0)
458}
459
460fn log_discv5_enr(discv5: &Discv5) {
461 let enr = discv5.local_enr();
462 info!(
463 id = ?enr.node_id(),
464 ip4 = ?enr.ip4(),
465 ip6 = ?enr.ip6(),
466 udp4 = ?enr.udp4(),
467 udp6 = ?enr.udp6(),
468 tcp4 = ?enr.tcp4(),
469 tcp6 = ?enr.tcp6(),
470 "(Discv5) advertised endpoints",
471 );
472 info!("(Discv5) enr: {enr}");
473 if let Some(record) = discv5.node_record() {
474 info!("(Discv5) enode: {record}");
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use reth_discv5::build_local_enr;
482
483 #[test]
484 fn repeated_nat_uses_matching_addr_family_as_primary() {
485 let command = Command::parse_from([
486 "reth",
487 "--addr",
488 "[::]:30303",
489 "--nat",
490 "extip:1.2.3.4",
491 "--nat",
492 "extip:2001:db8::1",
493 ]);
494
495 let nat = command.resolved_nat().unwrap();
496
497 assert_eq!(nat.resolver, NatResolver::ExternalIp("2001:db8::1".parse().unwrap()));
500 assert_eq!(nat.advertised_ips.len(), 2);
501 }
502
503 #[test]
504 fn repeated_nat_requires_extip_values() {
505 let command = Command::parse_from(["reth", "--nat", "any", "--nat", "extip:2001:db8::1"]);
506
507 assert!(command.resolved_nat().is_err());
508 }
509
510 #[test]
511 fn repeated_nat_requires_distinct_ip_families() {
512 let command =
513 Command::parse_from(["reth", "--nat", "extip:1.2.3.4", "--nat", "extip:5.6.7.8"]);
514
515 assert!(command.resolved_nat().is_err());
516 }
517
518 #[test]
519 fn single_extip_is_advertised() {
520 let command =
521 Command::parse_from(["reth", "--addr", "0.0.0.0:30301", "--nat", "extip:1.2.3.4"]);
522
523 let nat = command.resolved_nat().unwrap();
524
525 assert_eq!(nat.advertised_ips, vec!["1.2.3.4".parse::<IpAddr>().unwrap()]);
526 }
527
528 #[test]
529 fn discv5_advertises_single_extip_of_other_family_than_addr() {
530 let command = Command::parse_from([
532 "reth",
533 "--addr",
534 "0.0.0.0:45678",
535 "--v5",
536 "--nat",
537 "extip:2001:db8::1",
538 ]);
539 let nat = command.resolved_nat().unwrap();
540 let config = command.discv5_config(&nat, &command.seed_nodes(&nat).unwrap());
541 let sk = SecretKey::from_byte_array(&[1u8; 32]).unwrap();
542
543 let (enr, _, _, _) = build_local_enr(&sk, &config);
544
545 assert_eq!(enr.ip6(), Some("2001:db8::1".parse().unwrap()));
547 assert_eq!(enr.udp6(), Some(45678));
548 }
549
550 #[test]
551 fn discv5_config_single_stack_ipv4_uses_addr_port() {
552 let command = Command::parse_from([
554 "reth",
555 "--addr",
556 "0.0.0.0:45678",
557 "--v5",
558 "--nat",
559 "extip:1.2.3.4",
560 ]);
561 let nat = command.resolved_nat().unwrap();
562 let config = command.discv5_config(&nat, &command.seed_nodes(&nat).unwrap());
563 let sk = SecretKey::from_byte_array(&[1u8; 32]).unwrap();
564
565 let (enr, _, _, _) = build_local_enr(&sk, &config);
566
567 assert_eq!(enr.ip4(), Some("1.2.3.4".parse().unwrap()));
568 assert_eq!(enr.udp4(), Some(45678));
569 assert_eq!(enr.udp6(), None);
570 }
571
572 #[test]
573 fn discv5_config_single_stack_ipv6_uses_addr_port() {
574 let command = Command::parse_from([
576 "reth",
577 "--addr",
578 "[::]:45678",
579 "--v5",
580 "--nat",
581 "extip:2001:db8::1",
582 ]);
583 let nat = command.resolved_nat().unwrap();
584 let config = command.discv5_config(&nat, &command.seed_nodes(&nat).unwrap());
585 let sk = SecretKey::from_byte_array(&[1u8; 32]).unwrap();
586
587 let (enr, _, _, _) = build_local_enr(&sk, &config);
588
589 assert_eq!(enr.ip6(), Some("2001:db8::1".parse().unwrap()));
590 assert_eq!(enr.udp6(), Some(45678));
591 assert_eq!(enr.udp4(), None);
592 }
593
594 #[test]
595 fn single_opposite_family_extip_does_not_drive_discv4() {
596 let command = Command::parse_from([
599 "reth",
600 "--addr",
601 "0.0.0.0:45678",
602 "--v5",
603 "--nat",
604 "extip:2001:db8::1",
605 ]);
606 let nat = command.resolved_nat().unwrap();
607
608 assert_eq!(nat.resolver, NatResolver::None);
609 assert_eq!(nat.advertised_ips, vec!["2001:db8::1".parse::<IpAddr>().unwrap()]);
610 }
611
612 #[tokio::test]
613 async fn shared_socket_binds_both_families_on_one_port() {
614 let (Ok(probe4), Ok(probe6)) = (
617 bind_socket("0.0.0.0:0".parse().unwrap()).await,
618 bind_socket("[::]:0".parse().unwrap()).await,
619 ) else {
620 return;
621 };
622 drop((probe4, probe6));
623
624 let v4 = bind_socket("0.0.0.0:0".parse().unwrap()).await.unwrap();
625 let port = v4.local_addr().unwrap().port();
626 let v6 = bind_socket(format!("[::]:{port}").parse().unwrap()).await.unwrap();
627 assert_eq!(v6.local_addr().unwrap().port(), port);
628 }
629
630 #[test]
631 fn discv4_config_serves_secondary_family_only_with_v5() {
632 let args = [
635 "reth",
636 "--addr",
637 "0.0.0.0:30303",
638 "--nat",
639 "extip:1.2.3.4",
640 "--nat",
641 "extip:2001:db8::1",
642 ];
643
644 let command = Command::parse_from(args);
645 let nat = command.resolved_nat().unwrap();
646 let config = command.discv4_config(&nat, &command.seed_nodes(&nat).unwrap());
647 assert_eq!(nat.resolver, NatResolver::ExternalIp("1.2.3.4".parse().unwrap()));
648 assert_eq!(config.secondary_advertised_ip, None);
649
650 let command = Command::parse_from(args.iter().chain(&["--v5"]));
651 let nat = command.resolved_nat().unwrap();
652 let config = command.discv4_config(&nat, &command.seed_nodes(&nat).unwrap());
653 assert_eq!(nat.resolver, NatResolver::ExternalIp("1.2.3.4".parse().unwrap()));
654 assert_eq!(config.secondary_advertised_ip, Some("2001:db8::1".parse().unwrap()));
655 }
656
657 #[test]
658 fn bootnodes_seed_discv4_and_discv5() {
659 let enr = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8";
661 let enode = "enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@1.2.3.4:0?discport=30303";
662
663 let command = Command::parse_from(["reth", "--bootnodes", &format!("{enode},{enr}")]);
664 let nat = command.resolved_nat().unwrap();
665 let seeds = command.seed_nodes(&nat).unwrap();
666
667 assert_eq!(seeds.enodes.len(), 1);
668 assert_eq!(seeds.enodes[0].address, "1.2.3.4".parse::<IpAddr>().unwrap());
669 assert_eq!(seeds.enodes[0].udp_port, 30303);
670 assert_eq!(seeds.enodes[0].tcp_port, 0);
671 assert_eq!(seeds.enr_records.len(), 1);
674 assert_eq!(seeds.enr_records[0].address, "127.0.0.1".parse::<IpAddr>().unwrap());
675 assert_eq!(seeds.enr_records[0].udp_port, 30303);
676 assert_eq!(seeds.enr_records[0].tcp_port, 0);
677 assert_eq!(seeds.enrs.len(), 1);
678 assert_eq!(seeds.enrs[0].udp4(), Some(30303));
679
680 let config = command.discv4_config(&command.resolved_nat().unwrap(), &seeds);
681 assert_eq!(config.bootstrap_nodes.len(), 2);
682 }
683
684 #[test]
685 fn discv4_config_filters_unserved_bootnodes() {
686 let peer_id = "d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666";
687 let command = Command::parse_from([
688 "reth",
689 "--addr",
690 "0.0.0.0:30303",
691 "--bootnodes",
692 &format!("enode://{peer_id}@[2001:db8::1]:0?discport=30303"),
693 ]);
694 let nat = command.resolved_nat().unwrap();
695 let seeds = command.seed_nodes(&nat).unwrap();
696
697 let config = command.discv4_config(&nat, &seeds);
698
699 assert!(config.bootstrap_nodes.is_empty());
700 }
701
702 #[test]
703 fn bootnodes_normalize_and_validate_enodes() {
704 let peer_id = "d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666";
705
706 let command = Command::parse_from([
708 "reth",
709 "--bootnodes",
710 &format!("enode://{peer_id}@[::ffff:9.9.9.9]:0?discport=30303"),
711 ]);
712 let seeds = command.seed_nodes(&command.resolved_nat().unwrap()).unwrap();
713 assert_eq!(seeds.enodes[0].address, "9.9.9.9".parse::<IpAddr>().unwrap());
714
715 let command =
717 Command::parse_from(["reth", "--bootnodes", &format!("enode://{peer_id}@1.2.3.4:0")]);
718 assert!(command.seed_nodes(&command.resolved_nat().unwrap()).is_err());
719
720 let bad_id = "ff".repeat(64);
722 let command = Command::parse_from([
723 "reth",
724 "--bootnodes",
725 &format!("enode://{bad_id}@1.2.3.4:30303"),
726 ]);
727 assert!(command.seed_nodes(&command.resolved_nat().unwrap()).is_err());
728 }
729
730 #[test]
731 fn enr_seed_prefers_served_family() {
732 let dual = Enr::builder()
733 .ip4("9.9.9.9".parse().unwrap())
734 .udp4(30303)
735 .ip6("2001:db8::9".parse().unwrap())
736 .udp6(30304)
737 .build(&rng_secret_key())
738 .unwrap()
739 .to_base64();
740
741 let command = Command::parse_from([
743 "reth",
744 "--addr",
745 "[::]:30303",
746 "--nat",
747 "extip:2001:db8::1",
748 "--bootnodes",
749 &dual,
750 ]);
751 let seeds = command.seed_nodes(&command.resolved_nat().unwrap()).unwrap();
752 assert_eq!(seeds.enr_records[0].address, "2001:db8::9".parse::<IpAddr>().unwrap());
753 assert_eq!(seeds.enr_records[0].udp_port, 30304);
754
755 let command = Command::parse_from([
757 "reth",
758 "--addr",
759 "0.0.0.0:30303",
760 "--nat",
761 "extip:1.2.3.4",
762 "--bootnodes",
763 &dual,
764 ]);
765 let seeds = command.seed_nodes(&command.resolved_nat().unwrap()).unwrap();
766 assert_eq!(seeds.enr_records[0].address, "9.9.9.9".parse::<IpAddr>().unwrap());
767 assert_eq!(seeds.enr_records[0].udp_port, 30303);
768 }
769
770 #[test]
771 fn enr_seed_never_mixes_families() {
772 let sk = rng_secret_key();
773 let enr = Enr::builder()
774 .ip4("9.9.9.9".parse().unwrap())
775 .ip6("2001:db8::9".parse().unwrap())
776 .udp6(30304)
777 .build(&sk)
778 .unwrap();
779
780 let command = Command::parse_from([
783 "reth",
784 "--addr",
785 "0.0.0.0:30303",
786 "--nat",
787 "extip:1.2.3.4",
788 "--bootnodes",
789 &enr.to_base64(),
790 ]);
791 let seeds = command.seed_nodes(&command.resolved_nat().unwrap()).unwrap();
792 assert_eq!(seeds.enr_records[0].address, "2001:db8::9".parse::<IpAddr>().unwrap());
793 assert_eq!(seeds.enr_records[0].udp_port, 30304);
794
795 let enr = Enr::builder().ip4("9.9.9.9".parse().unwrap()).build(&sk).unwrap();
797 let command = Command::parse_from([
798 "reth",
799 "--addr",
800 "0.0.0.0:30303",
801 "--nat",
802 "extip:1.2.3.4",
803 "--bootnodes",
804 &enr.to_base64(),
805 ]);
806 let err = command.seed_nodes(&command.resolved_nat().unwrap()).unwrap_err();
807 assert_eq!(
808 err.to_string(),
809 format!("unusable --bootnodes ENR (no ip+udp endpoint for one family): {enr}")
810 );
811 }
812
813 #[test]
814 fn enr_seed_uses_generic_ports_for_ip6() {
815 let sk = rng_secret_key();
816 let enr = Enr::builder()
817 .ip6("2001:db8::9".parse().unwrap())
818 .udp4(30305)
819 .tcp4(30306)
820 .build(&sk)
821 .unwrap();
822
823 let command =
824 Command::parse_from(["reth", "--addr", "[::]:30303", "--bootnodes", &enr.to_base64()]);
825 let seeds = command.seed_nodes(&command.resolved_nat().unwrap()).unwrap();
826 assert_eq!(seeds.enr_records[0].address, "2001:db8::9".parse::<IpAddr>().unwrap());
827 assert_eq!(seeds.enr_records[0].udp_port, 30305);
828 assert_eq!(seeds.enr_records[0].tcp_port, 30306);
829 }
830
831 #[test]
832 fn enr_seed_skips_zero_udp_family() {
833 let sk = rng_secret_key();
834 let enr = Enr::builder()
835 .ip4("9.9.9.9".parse().unwrap())
836 .udp4(0)
837 .ip6("2001:db8::9".parse().unwrap())
838 .udp6(30304)
839 .build(&sk)
840 .unwrap();
841
842 let command = Command::parse_from([
844 "reth",
845 "--addr",
846 "0.0.0.0:30303",
847 "--nat",
848 "extip:1.2.3.4",
849 "--bootnodes",
850 &enr.to_base64(),
851 ]);
852 let seeds = command.seed_nodes(&command.resolved_nat().unwrap()).unwrap();
853 assert_eq!(seeds.enr_records[0].address, "2001:db8::9".parse::<IpAddr>().unwrap());
854 assert_eq!(seeds.enr_records[0].udp_port, 30304);
855
856 assert!(!discv5_contactable(&seeds.enrs[0], true, false));
858 assert!(discv5_contactable(&seeds.enrs[0], false, true));
859
860 let enr = Enr::builder().ip4("9.9.9.9".parse().unwrap()).udp4(0).build(&sk).unwrap();
862 let command = Command::parse_from([
863 "reth",
864 "--addr",
865 "0.0.0.0:30303",
866 "--bootnodes",
867 &enr.to_base64(),
868 ]);
869 let err = command.seed_nodes(&command.resolved_nat().unwrap()).unwrap_err();
870 assert_eq!(err.to_string(), format!("--bootnodes entry has no discovery port: {enr}"));
871 }
872
873 #[test]
874 fn enr_seed_normalizes_v4_mapped_ip6() {
875 let sk = rng_secret_key();
876 let enr =
877 Enr::builder().ip6("::ffff:1.2.3.4".parse().unwrap()).udp6(30303).build(&sk).unwrap();
878
879 let command = Command::parse_from([
880 "reth",
881 "--addr",
882 "0.0.0.0:30303",
883 "--nat",
884 "extip:9.9.9.9",
885 "--bootnodes",
886 &enr.to_base64(),
887 ]);
888 let nat = command.resolved_nat().unwrap();
889 let seeds = command.seed_nodes(&nat).unwrap();
890 assert_eq!(seeds.enr_records[0].address, "1.2.3.4".parse::<IpAddr>().unwrap());
891 assert_eq!(seeds.enr_records[0].udp_port, 30303);
892 assert_eq!(command.discv4_config(&nat, &seeds).bootstrap_nodes.len(), 1);
893
894 assert!(!discv5_contactable(&seeds.enrs[0], true, true));
895 assert!(!discv5_contactable(&seeds.enrs[0], false, true));
896
897 let canonical =
898 Enr::builder().ip6("2001:db8::9".parse().unwrap()).udp6(30303).build(&sk).unwrap();
899 let canonical = EnrCombinedKeyWrapper::from(canonical).0;
900 assert!(discv5_contactable(&canonical, false, true));
901 }
902
903 #[test]
904 fn bootnodes_reject_endpointless_entries() {
905 let peer_id = "d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666";
906
907 let command = Command::parse_from(["reth", "--bootnodes", &format!("enode://{peer_id}")]);
908 assert!(command.seed_nodes(&command.resolved_nat().unwrap()).is_err());
909 }
910
911 #[test]
912 fn discv5_config_advertises_dual_stack_nat_endpoints() {
913 let command = Command::parse_from([
914 "reth",
915 "--addr",
916 "0.0.0.0:45678",
917 "--v5",
918 "--nat",
919 "extip:1.2.3.4",
920 "--nat",
921 "extip:2001:db8::1",
922 ]);
923 let nat = command.resolved_nat().unwrap();
924 let config = command.discv5_config(&nat, &command.seed_nodes(&nat).unwrap());
925 let sk = SecretKey::from_byte_array(&[1u8; 32]).unwrap();
926
927 let (enr, _, _, _) = build_local_enr(&sk, &config);
928
929 assert_eq!(enr.ip4(), Some("1.2.3.4".parse().unwrap()));
931 assert_eq!(enr.ip6(), Some("2001:db8::1".parse().unwrap()));
932 assert_eq!(enr.udp4(), Some(45678));
933 assert_eq!(enr.udp6(), Some(45678));
934 assert_eq!(enr.tcp4(), Some(0));
936 assert_eq!(enr.tcp6(), None);
937
938 let record = NodeRecord::try_from(&enr).unwrap();
940 assert_eq!(record.tcp_port, 0);
941 assert_eq!(record.udp_port, 45678);
942 }
943}