1use std::collections::{HashMap, HashSet};
14use std::sync::Arc;
15use std::time::{Duration, SystemTime, UNIX_EPOCH};
16
17use anemo::{Network, PeerId};
18use consensus_config::{
19 Authority as ConsensusAuthority, Committee as ConsensusCommittee,
20 NetworkKeyPair as ConsensusNetworkKeyPair, NetworkPublicKey as ConsensusNetworkPublicKey,
21};
22use fastcrypto::encoding::{Encoding, Hex};
23use futures::future::{BoxFuture, FutureExt, join_all};
24use futures::stream::{FuturesUnordered, StreamExt};
25use mysten_metrics::spawn_monitored_task;
26use mysten_network::Multiaddr;
27use prometheus::{
28 IntCounterVec, IntGaugeVec, Registry, register_int_counter_vec_with_registry,
29 register_int_gauge_vec_with_registry,
30};
31use serde::Serialize;
32use sui_config::AddressProberConfig;
33use sui_core::consensus_manager::ConsensusManager;
34use sui_network::discovery::{Sender as DiscoverySender, TrustedPeerP2pAddresses};
35use sui_network::endpoint_manager::AddressSource;
36use tokio::sync::{Semaphore, mpsc, oneshot};
37use tokio::time::Instant;
38use tracing::{debug, info};
39
40const MAILBOX_CAPACITY: usize = 128; #[derive(Clone, Copy, PartialEq, Eq)]
45enum EndpointType {
46 P2p,
47 Consensus,
48}
49
50impl EndpointType {
51 fn as_str(self) -> &'static str {
52 match self {
53 EndpointType::P2p => "p2p",
54 EndpointType::Consensus => "consensus",
55 }
56 }
57}
58
59#[derive(Clone, Copy, PartialEq, Eq)]
62enum ProbeResult {
63 Reachable,
64 Unreachable,
65 WrongIdentity,
66 BadAddress,
67 Timeout,
68 }
70
71impl ProbeResult {
72 const ALL: [ProbeResult; 5] = [
75 ProbeResult::Reachable,
76 ProbeResult::Unreachable,
77 ProbeResult::WrongIdentity,
78 ProbeResult::BadAddress,
79 ProbeResult::Timeout,
80 ];
81
82 fn is_reachable(self) -> bool {
83 matches!(self, ProbeResult::Reachable)
84 }
85
86 fn as_str(self) -> &'static str {
87 match self {
88 ProbeResult::Reachable => "reachable",
89 ProbeResult::Unreachable => "unreachable",
90 ProbeResult::WrongIdentity => "wrong_identity",
91 ProbeResult::BadAddress => "bad_address",
92 ProbeResult::Timeout => "timeout",
93 }
94 }
95}
96
97impl From<anemo::ProbeOutcome> for ProbeResult {
98 fn from(outcome: anemo::ProbeOutcome) -> Self {
99 match outcome {
100 anemo::ProbeOutcome::Reachable => ProbeResult::Reachable,
101 anemo::ProbeOutcome::Unreachable => ProbeResult::Unreachable,
102 anemo::ProbeOutcome::WrongIdentity => ProbeResult::WrongIdentity,
103 anemo::ProbeOutcome::BadAddress => ProbeResult::BadAddress,
104 anemo::ProbeOutcome::Timeout => ProbeResult::Timeout,
105 }
106 }
107}
108
109#[derive(Clone)]
111enum AddressTarget {
112 P2p {
113 peer_id: PeerId,
114 address: anemo::types::Address,
115 },
116 Consensus {
117 target_key: ConsensusNetworkPublicKey,
118 address: Multiaddr,
119 },
120}
121
122impl AddressTarget {
123 fn display(&self) -> String {
124 match self {
125 AddressTarget::P2p { address, .. } => address.to_string(),
126 AddressTarget::Consensus { address, .. } => address.to_string(),
127 }
128 }
129}
130
131#[derive(Clone)]
135struct AuthorityInfo {
136 authority_name: String,
139 hostname: String,
141}
142
143struct ProbeGroup {
145 peer_label: String,
146 endpoint_type: EndpointType,
147 source: AddressSource,
148 authority: Option<AuthorityInfo>,
150 addresses: Vec<AddressTarget>,
151}
152
153type GroupKey = (String, &'static str, &'static str);
155
156struct AddressOutcome {
159 address: String,
160 result: ProbeResult,
161}
162
163struct Group {
165 peer_label: String,
166 endpoint_type: EndpointType,
167 source: AddressSource,
168 authority: Option<AuthorityInfo>,
170 targets: Vec<AddressTarget>,
172 probing: bool,
174 last_probed: Option<Instant>,
175 consecutive_failures: u32,
176 connectable: bool,
179 last_success_unix_secs: Option<i64>,
180 outcomes: Vec<AddressOutcome>,
181}
182
183impl Group {
184 fn new(candidate: ProbeGroup) -> Self {
185 Self {
186 peer_label: candidate.peer_label,
187 endpoint_type: candidate.endpoint_type,
188 source: candidate.source,
189 authority: candidate.authority,
190 targets: candidate.addresses,
191 probing: false,
192 last_probed: None,
193 consecutive_failures: 0,
194 connectable: false,
195 last_success_unix_secs: None,
196 outcomes: Vec::new(),
197 }
198 }
199
200 fn next_due(
203 &self,
204 good_interval: Duration,
205 failed_interval: Duration,
206 now: Instant,
207 ) -> Instant {
208 match self.last_probed {
209 None => now,
210 Some(last_probed) => {
211 let interval = if self.consecutive_failures == 0 {
212 good_interval
213 } else {
214 failed_interval
215 };
216 last_probed + interval
217 }
218 }
219 }
220}
221
222pub struct ProberEpochContext {
224 pub epoch: u64,
225 pub consensus_committee: ConsensusCommittee,
226 pub consensus_manager: Arc<ConsensusManager>,
227}
228
229enum ProberMessage {
230 UpdateEpoch {
233 epoch: u64,
234 consensus_committee: ConsensusCommittee,
235 consensus_manager: Arc<ConsensusManager>,
236 },
237 LeaveCommittee,
239 GetReport { reply: oneshot::Sender<ProbeReport> },
241}
242
243#[derive(Clone, Debug, Serialize)]
245pub struct ProbeReport {
246 pub epoch: Option<u64>,
247 pub groups: Vec<ProbeGroupReport>,
248}
249
250#[derive(Clone, Debug, Serialize)]
252pub struct ProbeGroupReport {
253 pub peer: String,
255 pub authority_name: Option<String>,
258 pub hostname: Option<String>,
260 pub endpoint_type: String,
261 pub address_source: String,
262 pub connectable: bool,
264 pub consecutive_failures: u32,
265 pub seconds_since_last_probe: u64,
267 pub last_success_unix_seconds: Option<i64>,
269 pub addresses: Vec<ProbeAddressReport>,
270}
271
272#[derive(Clone, Debug, Serialize)]
274pub struct ProbeAddressReport {
275 pub address: String,
276 pub result: String,
278}
279
280pub struct AddressProberMetrics {
281 connectable: IntGaugeVec,
282 last_success_timestamp_seconds: IntGaugeVec,
283 attempts_total: IntCounterVec,
284}
285
286impl AddressProberMetrics {
287 pub fn new(registry: &Registry) -> Arc<Self> {
288 Arc::new(Self {
289 connectable: register_int_gauge_vec_with_registry!(
290 "discovery_probe_connectable",
291 "1 if a trusted peer's advertised address for this endpoint/source is connectable, \
292 0 after N consecutive failed probe cycles",
293 &["peer_id", "endpoint_type", "address_source"],
294 registry
295 )
296 .unwrap(),
297 last_success_timestamp_seconds: register_int_gauge_vec_with_registry!(
298 "discovery_probe_last_success_timestamp_seconds",
299 "Unix timestamp (seconds) of the last successful probe for this peer/endpoint/source",
300 &["peer_id", "endpoint_type", "address_source"],
301 registry
302 )
303 .unwrap(),
304 attempts_total: register_int_counter_vec_with_registry!(
305 "discovery_probe_attempts_total",
306 "Total address probe attempts by peer/endpoint/source and result",
307 &["peer_id", "endpoint_type", "address_source", "result"],
308 registry
309 )
310 .unwrap(),
311 })
312 }
313}
314
315#[cfg(any(test, msim))]
316impl AddressProberMetrics {
317 pub fn connectable_for_testing(
320 &self,
321 peer_label: &str,
322 endpoint_type: &str,
323 source: &str,
324 ) -> i64 {
325 self.connectable
326 .with_label_values(&[peer_label, endpoint_type, source])
327 .get()
328 }
329
330 pub fn attempts_value_for_testing(
332 &self,
333 peer_label: &str,
334 endpoint_type: &str,
335 source: &str,
336 result: &str,
337 ) -> u64 {
338 self.attempts_total
339 .with_label_values(&[peer_label, endpoint_type, source, result])
340 .get()
341 }
342
343 pub fn total_attempts_for_testing(
345 &self,
346 peer_label: &str,
347 endpoint_type: &str,
348 source: &str,
349 ) -> u64 {
350 ProbeResult::ALL
351 .into_iter()
352 .map(|result| {
353 self.attempts_value_for_testing(peer_label, endpoint_type, source, result.as_str())
354 })
355 .sum()
356 }
357}
358
359#[derive(Clone)]
362pub struct Handle {
363 sender: mpsc::Sender<ProberMessage>,
364 #[cfg(any(test, msim))]
366 metrics: Arc<AddressProberMetrics>,
367}
368
369impl Handle {
370 pub fn update_epoch(
372 &self,
373 epoch: u64,
374 consensus_committee: ConsensusCommittee,
375 consensus_manager: Arc<ConsensusManager>,
376 ) {
377 self.sender
378 .try_send(ProberMessage::UpdateEpoch {
379 epoch,
380 consensus_committee,
381 consensus_manager,
382 })
383 .expect("address prober mailbox should not overflow or be closed");
384 }
385
386 pub fn leave_committee(&self) {
388 self.sender
389 .try_send(ProberMessage::LeaveCommittee)
390 .expect("address prober mailbox should not overflow or be closed");
391 }
392
393 pub async fn probe_report(&self) -> Option<ProbeReport> {
396 let (reply, response) = oneshot::channel();
397 if self
398 .sender
399 .send(ProberMessage::GetReport { reply })
400 .await
401 .is_err()
402 {
403 return None;
404 }
405 response.await.ok()
406 }
407
408 #[cfg(any(test, msim))]
409 pub fn metrics_for_testing(&self) -> Arc<AddressProberMetrics> {
410 self.metrics.clone()
411 }
412}
413
414pub struct Builder {
415 config: AddressProberConfig,
416 metrics: Option<Arc<AddressProberMetrics>>,
417}
418
419impl Default for Builder {
420 fn default() -> Self {
421 Self::new()
422 }
423}
424
425impl Builder {
426 pub fn new() -> Self {
427 Self {
428 config: AddressProberConfig::default(),
429 metrics: None,
430 }
431 }
432
433 pub fn config(mut self, config: AddressProberConfig) -> Self {
434 self.config = config;
435 self
436 }
437
438 pub fn with_metrics(mut self, registry: &Registry) -> Self {
439 self.metrics = Some(AddressProberMetrics::new(registry));
440 self
441 }
442
443 pub fn build(self) -> UnstartedAddressProber {
444 let metrics = self
445 .metrics
446 .unwrap_or_else(|| AddressProberMetrics::new(&Registry::new()));
447 let (sender, mailbox) = mpsc::channel(MAILBOX_CAPACITY);
448 let handle = Handle {
449 sender,
450 #[cfg(any(test, msim))]
451 metrics: metrics.clone(),
452 };
453 UnstartedAddressProber {
454 config: self.config,
455 metrics,
456 handle,
457 mailbox,
458 }
459 }
460}
461
462pub struct UnstartedAddressProber {
467 config: AddressProberConfig,
468 metrics: Arc<AddressProberMetrics>,
469 handle: Handle,
470 mailbox: mpsc::Receiver<ProberMessage>,
471}
472
473impl UnstartedAddressProber {
474 pub fn start(
476 self,
477 network: Network,
478 discovery: DiscoverySender,
479 own_consensus_keypair: ConsensusNetworkKeyPair,
480 ) -> Handle {
481 let event_loop = AddressProberEventLoop::new(
482 self.config,
483 self.metrics,
484 self.mailbox,
485 network,
486 discovery,
487 own_consensus_keypair,
488 );
489 spawn_monitored_task!(event_loop.start());
490 self.handle
491 }
492}
493
494struct AddressProberEventLoop {
495 good_interval: Duration,
497 failed_interval: Duration,
498 failure_threshold: u32,
499 consensus_probe_timeout: Duration,
500 network: Network,
502 discovery: DiscoverySender,
503 own_consensus_keypair: ConsensusNetworkKeyPair,
505 own_peer_id: PeerId,
506 own_consensus_key: ConsensusNetworkPublicKey,
507 metrics: Arc<AddressProberMetrics>,
508 mailbox: mpsc::Receiver<ProberMessage>,
509 inflight_probe_limit: Arc<Semaphore>,
510 tasks: FuturesUnordered<BoxFuture<'static, (GroupKey, Vec<AddressOutcome>)>>,
512 groups: HashMap<GroupKey, Group>,
513 epoch_state: Option<Arc<ProberEpochContext>>,
515}
516
517impl AddressProberEventLoop {
518 fn new(
519 config: AddressProberConfig,
520 metrics: Arc<AddressProberMetrics>,
521 mailbox: mpsc::Receiver<ProberMessage>,
522 network: Network,
523 discovery: DiscoverySender,
524 own_consensus_keypair: ConsensusNetworkKeyPair,
525 ) -> Self {
526 let own_peer_id = network.peer_id();
527 let own_consensus_key = own_consensus_keypair.public();
528 Self {
529 good_interval: config.good_interval(),
530 failed_interval: config.failed_interval(),
531 failure_threshold: config.failure_threshold(),
532 consensus_probe_timeout: config.consensus_probe_timeout(),
533 network,
534 discovery,
535 own_consensus_keypair,
536 own_peer_id,
537 own_consensus_key,
538 metrics,
539 mailbox,
540 inflight_probe_limit: Arc::new(Semaphore::new(config.concurrency())),
541 tasks: FuturesUnordered::new(),
542 groups: HashMap::new(),
543 epoch_state: None,
544 }
545 }
546
547 async fn start(mut self) {
548 info!(
549 good_interval_secs = self.good_interval.as_secs(),
550 failed_interval_secs = self.failed_interval.as_secs(),
551 "starting discovery address prober",
552 );
553
554 let mut timer = Box::pin(tokio::time::sleep_until(Instant::now()));
556 loop {
557 tokio::select! {
558 _ = &mut timer => self.scan().await,
559 Some((key, outcomes)) = self.tasks.next() => self.handle_probe_result(key, outcomes),
560 maybe_message = self.mailbox.recv() => match maybe_message {
561 Some(message) => self.handle_message(message),
563 None => break,
564 },
565 }
566 timer.as_mut().reset(self.next_deadline());
567 }
568
569 info!("discovery address prober ended");
570 }
571
572 fn handle_message(&mut self, message: ProberMessage) {
573 match message {
574 ProberMessage::UpdateEpoch {
575 epoch,
576 consensus_committee,
577 consensus_manager,
578 } => {
579 self.epoch_state = Some(Arc::new(ProberEpochContext {
580 epoch,
581 consensus_committee,
582 consensus_manager,
583 }));
584 }
585 ProberMessage::LeaveCommittee => {
586 self.epoch_state = None;
587 for key in self.groups.keys().cloned().collect::<Vec<_>>() {
590 self.remove_group_metrics(&key);
591 }
592 self.groups.clear();
593 }
594 ProberMessage::GetReport { reply } => {
595 let _ = reply.send(self.build_report());
596 }
597 }
598 }
599
600 fn next_deadline(&self) -> Instant {
604 let now = Instant::now();
605 let cap = now + self.failed_interval;
606 if self.epoch_state.is_none() {
607 return cap;
608 }
609 self.groups
610 .values()
611 .filter(|group| !group.probing)
612 .map(|group| group.next_due(self.good_interval, self.failed_interval, now))
613 .min()
614 .map_or(cap, |deadline| deadline.min(cap))
615 .max(now)
616 }
617
618 async fn scan(&mut self) {
622 let Some(context) = self.epoch_state.clone() else {
623 return;
624 };
625 let epoch = context.epoch;
626 let trusted_p2p_addresses = self.discovery.trusted_peer_p2p_addresses().await;
627 let candidates = build_groups(
628 trusted_p2p_addresses,
629 &context.consensus_manager,
630 &context.consensus_committee,
631 &self.own_peer_id,
632 &self.own_consensus_key,
633 );
634 self.refresh_groups(candidates);
635
636 let now = Instant::now();
637 let due: Vec<GroupKey> = self
638 .groups
639 .iter()
640 .filter(|(_, group)| {
641 !group.probing
642 && group.next_due(self.good_interval, self.failed_interval, now) <= now
643 })
644 .map(|(key, _)| key.clone())
645 .collect();
646 for key in due {
647 self.spawn_probe(epoch, &key);
648 }
649 }
650
651 fn refresh_groups(&mut self, candidates: Vec<ProbeGroup>) {
655 let current: HashSet<GroupKey> = candidates.iter().map(group_key).collect();
656 let removed: Vec<GroupKey> = self
659 .groups
660 .iter()
661 .filter(|(key, group)| !current.contains(*key) && !group.probing)
662 .map(|(key, _)| key.clone())
663 .collect();
664 for key in removed {
665 self.remove_group_metrics(&key);
666 self.groups.remove(&key);
667 }
668 for candidate in candidates {
669 let key = group_key(&candidate);
670 match self.groups.get_mut(&key) {
671 Some(group) => {
672 group.targets = candidate.addresses;
673 group.authority = candidate.authority;
674 }
675 None => {
676 self.groups.insert(key, Group::new(candidate));
677 }
678 }
679 }
680 }
681
682 fn remove_group_metrics(&self, key: &GroupKey) {
685 let (peer, endpoint_type, source) = (key.0.as_str(), key.1, key.2);
686 let labels = [peer, endpoint_type, source];
687 let _ = self.metrics.connectable.remove_label_values(&labels);
688 let _ = self
689 .metrics
690 .last_success_timestamp_seconds
691 .remove_label_values(&labels);
692 for result in ProbeResult::ALL {
694 let _ = self.metrics.attempts_total.remove_label_values(&[
695 peer,
696 endpoint_type,
697 source,
698 result.as_str(),
699 ]);
700 }
701 }
702
703 fn spawn_probe(&mut self, epoch: u64, key: &GroupKey) {
705 let targets = {
706 let group = self
707 .groups
708 .get_mut(key)
709 .expect("a due group is present in the map");
710 group.probing = true;
711 group.targets.clone()
712 };
713 let key = key.clone();
714 let network = self.network.clone();
715 let own_consensus_keypair = self.own_consensus_keypair.clone();
716 let consensus_probe_timeout = self.consensus_probe_timeout;
717 let inflight_probe_limit = self.inflight_probe_limit.clone();
718 self.tasks.push(
719 async move {
720 let outcomes = join_all(targets.into_iter().map(|target| {
721 let network = network.clone();
722 let own_consensus_keypair = own_consensus_keypair.clone();
723 let inflight_probe_limit = inflight_probe_limit.clone();
724 async move {
725 let _permit = inflight_probe_limit
726 .acquire_owned()
727 .await
728 .expect("prober semaphore is never closed");
729 let address = target.display();
730 let result = probe_one(
731 &target,
732 &network,
733 &own_consensus_keypair,
734 epoch,
735 consensus_probe_timeout,
736 )
737 .await;
738 AddressOutcome { address, result }
739 }
740 }))
741 .await;
742 (key, outcomes)
743 }
744 .boxed(),
745 );
746 }
747
748 fn handle_probe_result(&mut self, key: GroupKey, outcomes: Vec<AddressOutcome>) {
749 let Some(group) = self.groups.get_mut(&key) else {
752 return;
753 };
754 group.probing = false;
755
756 let now = Instant::now();
757 let connectable = outcomes.iter().any(|outcome| outcome.result.is_reachable());
758 let peer_label = group.peer_label.clone();
759 let endpoint_type = group.endpoint_type.as_str();
760 let source = address_source_str(group.source);
761 let labels = [peer_label.as_str(), endpoint_type, source];
762
763 for outcome in &outcomes {
764 self.metrics
765 .attempts_total
766 .with_label_values(&[
767 peer_label.as_str(),
768 endpoint_type,
769 source,
770 outcome.result.as_str(),
771 ])
772 .inc();
773 debug!(
774 peer = %peer_label,
775 endpoint_type,
776 source,
777 address = %outcome.address,
778 result = outcome.result.as_str(),
779 "probed address"
780 );
781 }
782
783 if connectable {
786 self.metrics.connectable.with_label_values(&labels).set(1);
787 let timestamp = now_unix_seconds();
788 self.metrics
789 .last_success_timestamp_seconds
790 .with_label_values(&labels)
791 .set(timestamp);
792 group.connectable = true;
793 group.last_success_unix_secs = Some(timestamp);
794 group.consecutive_failures = 0;
795 } else {
796 group.consecutive_failures += 1;
797 if group.consecutive_failures >= self.failure_threshold {
798 self.metrics.connectable.with_label_values(&labels).set(0);
799 group.connectable = false;
800 }
801 }
802 group.last_probed = Some(now);
803 group.outcomes = outcomes;
804 }
805
806 fn build_report(&self) -> ProbeReport {
808 let now = Instant::now();
809 let mut groups: Vec<ProbeGroupReport> = self
810 .groups
811 .values()
812 .filter_map(|group| {
813 let last_probed = group.last_probed?;
814 Some(ProbeGroupReport {
815 peer: group.peer_label.clone(),
816 authority_name: group
817 .authority
818 .as_ref()
819 .map(|authority| authority.authority_name.clone()),
820 hostname: group
821 .authority
822 .as_ref()
823 .map(|authority| authority.hostname.clone()),
824 endpoint_type: group.endpoint_type.as_str().to_string(),
825 address_source: address_source_str(group.source).to_string(),
826 connectable: group.connectable,
827 consecutive_failures: group.consecutive_failures,
828 seconds_since_last_probe: now.duration_since(last_probed).as_secs(),
829 last_success_unix_seconds: group.last_success_unix_secs,
830 addresses: group
831 .outcomes
832 .iter()
833 .map(|outcome| ProbeAddressReport {
834 address: outcome.address.clone(),
835 result: outcome.result.as_str().to_string(),
836 })
837 .collect(),
838 })
839 })
840 .collect();
841
842 groups.sort_by(|a, b| {
844 b.consecutive_failures
845 .cmp(&a.consecutive_failures)
846 .then_with(|| a.endpoint_type.cmp(&b.endpoint_type))
847 .then_with(|| a.peer.cmp(&b.peer))
848 .then_with(|| a.address_source.cmp(&b.address_source))
849 });
850
851 ProbeReport {
852 epoch: self.epoch_state.as_ref().map(|context| context.epoch),
853 groups,
854 }
855 }
856}
857
858fn build_groups(
861 trusted_p2p_addresses: TrustedPeerP2pAddresses,
862 consensus_manager: &ConsensusManager,
863 committee: &ConsensusCommittee,
864 own_peer_id: &PeerId,
865 own_consensus_key: &ConsensusNetworkPublicKey,
866) -> Vec<ProbeGroup> {
867 let mut groups = Vec::new();
868
869 let authority_by_network_key: HashMap<[u8; 32], &ConsensusAuthority> = committee
874 .authorities()
875 .map(|(_, authority)| (authority.network_key.to_bytes(), authority))
876 .collect();
877
878 for (peer_id, sources) in trusted_p2p_addresses {
880 if &peer_id == own_peer_id {
881 continue;
882 }
883 let authority = authority_by_network_key
884 .get(&peer_id.0)
885 .copied()
886 .map(authority_info);
887 for (source, addresses) in sources {
888 if addresses.is_empty() {
889 continue;
890 }
891 groups.push(ProbeGroup {
892 peer_label: peer_id.to_string(),
893 endpoint_type: EndpointType::P2p,
894 source,
895 authority: authority.clone(),
896 addresses: addresses
897 .into_iter()
898 .map(|address| AddressTarget::P2p { peer_id, address })
899 .collect(),
900 });
901 }
902 }
903
904 for (network_key, sources) in consensus_manager.address_overrides_snapshot() {
906 if &network_key == own_consensus_key {
907 continue;
908 }
909 let authority = authority_by_network_key
910 .get(&network_key.to_bytes())
911 .copied()
912 .map(authority_info);
913 for (source, addresses) in sources {
914 if addresses.is_empty() {
915 continue;
916 }
917 groups.push(ProbeGroup {
918 peer_label: consensus_peer_label(&network_key),
919 endpoint_type: EndpointType::Consensus,
920 source,
921 authority: authority.clone(),
922 addresses: addresses
923 .into_iter()
924 .map(|address| AddressTarget::Consensus {
925 target_key: network_key.clone(),
926 address,
927 })
928 .collect(),
929 });
930 }
931 }
932
933 for (_index, authority) in committee.authorities() {
935 if &authority.network_key == own_consensus_key {
936 continue;
937 }
938 groups.push(ProbeGroup {
939 peer_label: consensus_peer_label(&authority.network_key),
940 endpoint_type: EndpointType::Consensus,
941 source: AddressSource::Chain,
942 authority: Some(authority_info(authority)),
943 addresses: vec![AddressTarget::Consensus {
944 target_key: authority.network_key.clone(),
945 address: authority.address.clone(),
946 }],
947 });
948 }
949
950 groups
951}
952
953fn authority_info(authority: &ConsensusAuthority) -> AuthorityInfo {
955 AuthorityInfo {
956 authority_name: Hex::encode(authority.authority_name.to_bytes()),
957 hostname: authority.hostname.clone(),
958 }
959}
960
961async fn probe_one(
962 target: &AddressTarget,
963 network: &Network,
964 own_consensus_keypair: &ConsensusNetworkKeyPair,
965 epoch: u64,
966 consensus_probe_timeout: Duration,
967) -> ProbeResult {
968 match target {
969 AddressTarget::P2p { peer_id, address } => network
970 .probe_address(address.clone(), *peer_id)
971 .await
972 .into(),
973 AddressTarget::Consensus {
974 target_key,
975 address,
976 } => {
977 probe_consensus_address(
978 own_consensus_keypair,
979 target_key,
980 epoch,
981 address,
982 consensus_probe_timeout,
983 )
984 .await
985 }
986 }
987}
988
989async fn probe_consensus_address(
995 own_consensus_keypair: &ConsensusNetworkKeyPair,
996 target_key: &ConsensusNetworkPublicKey,
997 epoch: u64,
998 address: &Multiaddr,
999 timeout: Duration,
1000) -> ProbeResult {
1001 let Some(host_port) = consensus_host_port(address) else {
1002 return ProbeResult::BadAddress;
1003 };
1004 let uri = format!("https://{host_port}");
1005
1006 let server_name = format!("consensus_epoch_{epoch}");
1008 let client_tls_config = sui_tls::create_rustls_client_config(
1009 target_key.clone().into_inner(),
1010 server_name,
1011 Some(own_consensus_keypair.clone().private_key().into_inner()),
1012 );
1013
1014 let endpoint = match tonic_rustls::Channel::from_shared(uri) {
1015 Ok(endpoint) => endpoint.connect_timeout(timeout),
1016 Err(_) => return ProbeResult::BadAddress,
1017 };
1018 let endpoint = match endpoint.tls_config(client_tls_config) {
1019 Ok(endpoint) => endpoint,
1020 Err(_) => return ProbeResult::BadAddress,
1021 };
1022
1023 match tokio::time::timeout(timeout, endpoint.connect()).await {
1024 Ok(Ok(_channel)) => ProbeResult::Reachable,
1025 Ok(Err(_)) => ProbeResult::Unreachable,
1028 Err(_) => ProbeResult::Timeout,
1029 }
1030}
1031
1032fn consensus_host_port(addr: &Multiaddr) -> Option<String> {
1035 let host = addr.hostname()?;
1036 let port = addr.port()?;
1037 if host.contains(':') {
1038 Some(format!("[{host}]:{port}"))
1039 } else {
1040 Some(format!("{host}:{port}"))
1041 }
1042}
1043
1044fn group_key(group: &ProbeGroup) -> GroupKey {
1046 (
1047 group.peer_label.clone(),
1048 group.endpoint_type.as_str(),
1049 address_source_str(group.source),
1050 )
1051}
1052
1053fn consensus_peer_label(key: &ConsensusNetworkPublicKey) -> String {
1056 Hex::encode(key.to_bytes())
1057}
1058
1059#[cfg(any(test, msim))]
1062pub fn consensus_peer_label_for_testing(network_key_bytes: [u8; 32]) -> String {
1063 Hex::encode(network_key_bytes)
1064}
1065
1066fn address_source_str(source: AddressSource) -> &'static str {
1067 match source {
1068 AddressSource::Admin => "admin",
1069 AddressSource::Config => "config",
1070 AddressSource::Discovery => "discovery",
1071 AddressSource::Seed => "seed",
1072 AddressSource::Chain => "chain",
1073 }
1074}
1075
1076fn now_unix_seconds() -> i64 {
1077 SystemTime::now()
1078 .duration_since(UNIX_EPOCH)
1079 .map(|d| d.as_secs() as i64)
1080 .unwrap_or(0)
1081}