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