1use std::{
28 collections::{HashMap, HashSet},
29 fmt,
30 time::{Duration, Instant},
31 u32,
32};
33
34use futures::{channel::oneshot, select, FutureExt as _};
35use futures_timer::Delay;
36use rand::{Rng, SeedableRng};
37use rand_chacha::ChaCha20Rng;
38
39use sc_network::{config::parse_addr, Multiaddr};
40use sp_application_crypto::{AppCrypto, ByteArray};
41use sp_keystore::{Keystore, KeystorePtr};
42
43use polkadot_node_network_protocol::{
44 authority_discovery::AuthorityDiscovery, peer_set::PeerSet, GossipSupportNetworkMessage,
45 PeerId, ValidationProtocols,
46};
47use polkadot_node_subsystem::{
48 messages::{
49 ChainApiMessage, GossipSupportMessage, NetworkBridgeEvent, NetworkBridgeRxMessage,
50 NetworkBridgeTxMessage, RuntimeApiMessage, RuntimeApiRequest,
51 },
52 overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError,
53};
54use polkadot_node_subsystem_util as util;
55use polkadot_primitives::{AuthorityDiscoveryId, Hash, SessionIndex, SessionInfo, ValidatorIndex};
56
57#[cfg(test)]
58mod tests;
59
60mod metrics;
61
62use metrics::Metrics;
63
64const LOG_TARGET: &str = "parachain::gossip-support";
65#[cfg(not(test))]
68const BACKOFF_DURATION: Duration = Duration::from_secs(5);
69
70#[cfg(test)]
71const BACKOFF_DURATION: Duration = Duration::from_millis(500);
72
73#[cfg(not(test))]
78const TRY_RERESOLVE_AUTHORITIES: Duration = Duration::from_secs(5 * 60);
79
80#[cfg(test)]
81const TRY_RERESOLVE_AUTHORITIES: Duration = Duration::from_secs(2);
82
83const LOW_CONNECTIVITY_WARN_DELAY: Duration = Duration::from_secs(600);
91
92const LOW_CONNECTIVITY_WARN_THRESHOLD: usize = 85;
94
95pub struct GossipSupport<AD> {
97 keystore: KeystorePtr,
98
99 last_session_index: Option<SessionIndex>,
100 is_authority_now: bool,
102 min_known_session: SessionIndex,
104 last_failure: Option<Instant>,
108
109 last_connection_request: Option<Instant>,
116
117 failure_start: Option<Instant>,
123
124 resolved_authorities: HashMap<AuthorityDiscoveryId, HashSet<Multiaddr>>,
128
129 connected_authorities: HashMap<AuthorityDiscoveryId, PeerId>,
131 connected_peers: HashMap<PeerId, HashSet<AuthorityDiscoveryId>>,
135 authority_discovery: AD,
137
138 finalized_needed_session: Option<u32>,
141 metrics: Metrics,
143}
144
145#[overseer::contextbounds(GossipSupport, prefix = self::overseer)]
146impl<AD> GossipSupport<AD>
147where
148 AD: AuthorityDiscovery,
149{
150 pub fn new(keystore: KeystorePtr, authority_discovery: AD, metrics: Metrics) -> Self {
152 metrics.on_is_not_authority();
154 metrics.on_is_not_parachain_validator();
155
156 Self {
157 keystore,
158 last_session_index: None,
159 last_failure: None,
160 last_connection_request: None,
161 failure_start: None,
162 resolved_authorities: HashMap::new(),
163 connected_authorities: HashMap::new(),
164 connected_peers: HashMap::new(),
165 min_known_session: u32::MAX,
166 authority_discovery,
167 finalized_needed_session: None,
168 is_authority_now: false,
169 metrics,
170 }
171 }
172
173 async fn run<Context>(mut self, mut ctx: Context) -> Self {
174 fn get_connectivity_check_delay() -> Delay {
175 Delay::new(LOW_CONNECTIVITY_WARN_DELAY)
176 }
177 let mut next_connectivity_check = get_connectivity_check_delay().fuse();
178 loop {
179 let message = select!(
180 _ = next_connectivity_check => {
181 self.check_connectivity();
182 next_connectivity_check = get_connectivity_check_delay().fuse();
183 continue
184 }
185 result = ctx.recv().fuse() =>
186 match result {
187 Ok(message) => message,
188 Err(e) => {
189 gum::debug!(
190 target: LOG_TARGET,
191 err = ?e,
192 "Failed to receive a message from Overseer, exiting",
193 );
194 return self
195 },
196 }
197 );
198 match message {
199 FromOrchestra::Communication {
200 msg: GossipSupportMessage::NetworkBridgeUpdate(ev),
201 } => self.handle_connect_disconnect(ev),
202 FromOrchestra::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate {
203 activated,
204 ..
205 })) => {
206 gum::trace!(target: LOG_TARGET, "active leaves signal");
207
208 let leaves = activated.into_iter().map(|a| a.hash);
209 if let Err(e) = self.handle_active_leaves(ctx.sender(), leaves).await {
210 gum::debug!(target: LOG_TARGET, error = ?e);
211 }
212 },
213 FromOrchestra::Signal(OverseerSignal::BlockFinalized(_hash, _number)) =>
214 if let Some(session_index) = self.last_session_index {
215 if let Err(e) = self
216 .build_topology_for_last_finalized_if_needed(
217 ctx.sender(),
218 session_index,
219 )
220 .await
221 {
222 gum::warn!(
223 target: LOG_TARGET,
224 "Failed to build topology for last finalized session: {:?}",
225 e
226 );
227 }
228 },
229 FromOrchestra::Signal(OverseerSignal::Conclude) => return self,
230 }
231 }
232 }
233
234 async fn handle_active_leaves(
237 &mut self,
238 sender: &mut impl overseer::GossipSupportSenderTrait,
239 leaves: impl Iterator<Item = Hash>,
240 ) -> Result<(), util::Error> {
241 for leaf in leaves {
242 let current_index = util::request_session_index_for_child(leaf, sender).await.await??;
243 let since_failure = self.last_failure.map(|i| i.elapsed()).unwrap_or_default();
244 let since_last_reconnect =
245 self.last_connection_request.map(|i| i.elapsed()).unwrap_or_default();
246
247 let force_request = since_failure >= BACKOFF_DURATION;
248 let re_resolve_authorities = since_last_reconnect >= TRY_RERESOLVE_AUTHORITIES;
249 let leaf_session = Some((current_index, leaf));
250 let maybe_new_session = match self.last_session_index {
251 Some(i) if current_index <= i => None,
252 _ => leaf_session,
253 };
254
255 let maybe_issue_connection = if force_request || re_resolve_authorities {
256 leaf_session
257 } else {
258 maybe_new_session
259 };
260
261 if let Some((session_index, relay_parent)) = maybe_issue_connection {
262 let session_info =
263 util::request_session_info(leaf, session_index, sender).await.await??;
264
265 let session_info = match session_info {
266 Some(s) => s,
267 None => {
268 gum::warn!(
269 relay_parent = ?leaf,
270 session_index = self.last_session_index,
271 "Failed to get session info.",
272 );
273
274 continue
275 },
276 };
277
278 let is_new_session = maybe_new_session.is_some();
281 if is_new_session {
282 gum::debug!(
283 target: LOG_TARGET,
284 %session_index,
285 "New session detected",
286 );
287 self.last_session_index = Some(session_index);
288 self.is_authority_now =
289 ensure_i_am_an_authority(&self.keystore, &session_info.discovery_keys)
290 .is_ok();
291 }
292
293 {
305 let mut connections = authorities_past_present_future(sender, leaf).await?;
306 self.last_connection_request = Some(Instant::now());
307 let connections =
310 if remove_all_controlled(&self.keystore, &mut connections) != 0 {
311 connections
312 } else {
313 Vec::new()
316 };
317
318 if force_request || is_new_session {
319 self.issue_connection_request(sender, connections).await;
320 } else if re_resolve_authorities {
321 self.issue_connection_request_to_changed(sender, connections).await;
322 }
323 }
324
325 if is_new_session {
326 if let Err(err) = self
327 .build_topology_for_last_finalized_if_needed(sender, session_index)
328 .await
329 {
330 gum::warn!(
331 target: LOG_TARGET,
332 "Failed to build topology for last finalized session: {:?}",
333 err
334 );
335 }
336
337 let our_index = self.get_key_index_and_update_metrics(&session_info)?;
339 update_gossip_topology(
340 sender,
341 our_index,
342 session_info.discovery_keys.clone(),
343 relay_parent,
344 session_index,
345 )
346 .await?;
347 }
348 self.update_authority_ids(sender, session_info.discovery_keys).await;
351 }
352 }
353 Ok(())
354 }
355
356 async fn build_topology_for_last_finalized_if_needed(
364 &mut self,
365 sender: &mut impl overseer::GossipSupportSenderTrait,
366 current_session_index: u32,
367 ) -> Result<(), util::Error> {
368 self.min_known_session = self.min_known_session.min(current_session_index);
369
370 if self
371 .finalized_needed_session
372 .map(|oldest_needed_session| oldest_needed_session < self.min_known_session)
373 .unwrap_or(true)
374 {
375 let (tx, rx) = oneshot::channel();
376 sender.send_message(ChainApiMessage::FinalizedBlockNumber(tx)).await;
377 let finalized_block_number = match rx.await? {
378 Ok(block_number) => block_number,
379 _ => return Ok(()),
380 };
381
382 let (tx, rx) = oneshot::channel();
383 sender
384 .send_message(ChainApiMessage::FinalizedBlockHash(finalized_block_number, tx))
385 .await;
386
387 let finalized_block_hash = match rx.await? {
388 Ok(Some(block_hash)) => block_hash,
389 _ => return Ok(()),
390 };
391
392 let finalized_session_index =
393 util::request_session_index_for_child(finalized_block_hash, sender)
394 .await
395 .await??;
396
397 if finalized_session_index < self.min_known_session &&
398 Some(finalized_session_index) != self.finalized_needed_session
399 {
400 gum::debug!(
401 target: LOG_TARGET,
402 ?finalized_block_hash,
403 ?finalized_block_number,
404 ?finalized_session_index,
405 "Building topology for finalized block session",
406 );
407
408 let finalized_session_info = match util::request_session_info(
409 finalized_block_hash,
410 finalized_session_index,
411 sender,
412 )
413 .await
414 .await??
415 {
416 Some(session_info) => session_info,
417 _ => return Ok(()),
418 };
419
420 let our_index = self.get_key_index_and_update_metrics(&finalized_session_info)?;
421 update_gossip_topology(
422 sender,
423 our_index,
424 finalized_session_info.discovery_keys.clone(),
425 finalized_block_hash,
426 finalized_session_index,
427 )
428 .await?;
429 }
430 self.finalized_needed_session = Some(finalized_session_index);
431 }
432 Ok(())
433 }
434
435 fn get_key_index_and_update_metrics(
439 &mut self,
440 session_info: &SessionInfo,
441 ) -> Result<usize, util::Error> {
442 let authority_check_result =
443 ensure_i_am_an_authority(&self.keystore, &session_info.discovery_keys);
444
445 match authority_check_result.as_ref() {
446 Ok(index) => {
447 gum::trace!(target: LOG_TARGET, "We are now an authority",);
448 self.metrics.on_is_authority();
449
450 let parachain_validators_this_session = session_info.validators.len();
452
453 if *index < parachain_validators_this_session {
457 gum::trace!(target: LOG_TARGET, "We are now a parachain validator",);
458 self.metrics.on_is_parachain_validator();
459 } else {
460 gum::trace!(target: LOG_TARGET, "We are no longer a parachain validator",);
461 self.metrics.on_is_not_parachain_validator();
462 }
463 },
464 Err(util::Error::NotAValidator) => {
465 gum::trace!(target: LOG_TARGET, "We are no longer an authority",);
466 self.metrics.on_is_not_authority();
467 self.metrics.on_is_not_parachain_validator();
468 },
469 Err(_) => {},
471 };
472
473 authority_check_result
474 }
475
476 async fn resolve_authorities(
477 &mut self,
478 authorities: Vec<AuthorityDiscoveryId>,
479 ) -> (Vec<HashSet<Multiaddr>>, HashMap<AuthorityDiscoveryId, HashSet<Multiaddr>>, usize) {
480 let mut validator_addrs = Vec::with_capacity(authorities.len());
481 let mut resolved = HashMap::with_capacity(authorities.len());
482 let mut failures = 0;
483
484 for authority in authorities {
485 if let Some(addrs) =
486 self.authority_discovery.get_addresses_by_authority_id(authority.clone()).await
487 {
488 validator_addrs.push(addrs.clone());
489 resolved.insert(authority, addrs);
490 } else {
491 failures += 1;
492 gum::debug!(
493 target: LOG_TARGET,
494 "Couldn't resolve addresses of authority: {:?}",
495 authority
496 );
497 }
498 }
499 (validator_addrs, resolved, failures)
500 }
501
502 async fn issue_connection_request_to_changed<Sender>(
503 &mut self,
504 sender: &mut Sender,
505 authorities: Vec<AuthorityDiscoveryId>,
506 ) where
507 Sender: overseer::GossipSupportSenderTrait,
508 {
509 let (_, resolved, _) = self.resolve_authorities(authorities).await;
510
511 let mut changed = Vec::new();
512
513 for (authority, new_addresses) in &resolved {
514 let new_peer_ids = new_addresses
515 .iter()
516 .flat_map(|addr| parse_addr(addr.clone()).ok().map(|(p, _)| p))
517 .collect::<HashSet<_>>();
518 match self.resolved_authorities.get(authority) {
519 Some(old_addresses) => {
520 let old_peer_ids = old_addresses
521 .iter()
522 .flat_map(|addr| parse_addr(addr.clone()).ok().map(|(p, _)| p))
523 .collect::<HashSet<_>>();
524 if !old_peer_ids.is_superset(&new_peer_ids) {
525 changed.push(new_addresses.clone());
526 }
527 },
528 None => changed.push(new_addresses.clone()),
529 }
530 }
531 gum::debug!(
532 target: LOG_TARGET,
533 num_changed = ?changed.len(),
534 ?changed,
535 "Issuing a connection request to changed validators"
536 );
537 if !changed.is_empty() {
538 self.resolved_authorities = resolved;
539
540 sender
541 .send_message(NetworkBridgeTxMessage::AddToResolvedValidators {
542 validator_addrs: changed,
543 peer_set: PeerSet::Validation,
544 })
545 .await;
546 }
547 }
548
549 async fn issue_connection_request<Sender>(
550 &mut self,
551 sender: &mut Sender,
552 authorities: Vec<AuthorityDiscoveryId>,
553 ) where
554 Sender: overseer::GossipSupportSenderTrait,
555 {
556 let num = authorities.len();
557
558 let (validator_addrs, resolved, failures) = self.resolve_authorities(authorities).await;
559
560 self.resolved_authorities = resolved;
561 gum::debug!(target: LOG_TARGET, %num, "Issuing a connection request");
562
563 sender
564 .send_message(NetworkBridgeTxMessage::ConnectToResolvedValidators {
565 validator_addrs,
566 peer_set: PeerSet::Validation,
567 })
568 .await;
569
570 if num != 0 && 3 * failures >= num {
573 let timestamp = Instant::now();
574 match self.failure_start {
575 None => self.failure_start = Some(timestamp),
576 Some(first) if first.elapsed() >= LOW_CONNECTIVITY_WARN_DELAY => {
577 gum::warn!(
578 target: LOG_TARGET,
579 connected = ?(num - failures),
580 target = ?num,
581 "Low connectivity - authority lookup failed for too many validators."
582 );
583 },
584 Some(_) => {
585 gum::debug!(
586 target: LOG_TARGET,
587 connected = ?(num - failures),
588 target = ?num,
589 "Low connectivity (due to authority lookup failures) - expected on startup."
590 );
591 },
592 }
593 self.last_failure = Some(timestamp);
594 } else {
595 self.last_failure = None;
596 self.failure_start = None;
597 };
598 }
599
600 async fn update_authority_ids<Sender>(
601 &mut self,
602 sender: &mut Sender,
603 authorities: Vec<AuthorityDiscoveryId>,
604 ) where
605 Sender: overseer::GossipSupportSenderTrait,
606 {
607 let mut authority_ids: HashMap<PeerId, HashSet<AuthorityDiscoveryId>> = HashMap::new();
608 for authority in authorities {
609 let peer_ids = self
610 .authority_discovery
611 .get_addresses_by_authority_id(authority.clone())
612 .await
613 .into_iter()
614 .flat_map(|list| list.into_iter())
615 .flat_map(|addr| parse_addr(addr).ok().map(|(p, _)| p))
616 .collect::<HashSet<_>>();
617
618 gum::trace!(
619 target: LOG_TARGET,
620 ?peer_ids,
621 ?authority,
622 "Resolved to peer ids"
623 );
624
625 for p in peer_ids {
626 authority_ids.entry(p).or_default().insert(authority.clone());
627 }
628 }
629
630 for (peer_id, current) in self.connected_peers.iter_mut() {
632 if !current.is_empty() && !authority_ids.contains_key(peer_id) {
634 sender
635 .send_message(NetworkBridgeRxMessage::UpdatedAuthorityIds {
636 peer_id: *peer_id,
637 authority_ids: HashSet::new(),
638 })
639 .await;
640
641 for a in current.drain() {
642 self.connected_authorities.remove(&a);
643 }
644 }
645 }
646
647 for (peer_id, new) in authority_ids {
649 if let Some(prev) = self.connected_peers.get(&peer_id).filter(|x| x != &&new) {
651 sender
652 .send_message(NetworkBridgeRxMessage::UpdatedAuthorityIds {
653 peer_id,
654 authority_ids: new.clone(),
655 })
656 .await;
657
658 prev.iter().for_each(|a| {
659 self.connected_authorities.remove(a);
660 });
661 new.iter().for_each(|a| {
662 self.connected_authorities.insert(a.clone(), peer_id);
663 });
664
665 self.connected_peers.insert(peer_id, new);
666 }
667 }
668 }
669
670 fn handle_connect_disconnect(&mut self, ev: NetworkBridgeEvent<GossipSupportNetworkMessage>) {
671 match ev {
672 NetworkBridgeEvent::PeerConnected(peer_id, _, _, o_authority) => {
673 if let Some(authority_ids) = o_authority {
674 authority_ids.iter().for_each(|a| {
675 self.connected_authorities.insert(a.clone(), peer_id);
676 });
677 self.connected_peers.insert(peer_id, authority_ids);
678 } else {
679 self.connected_peers.insert(peer_id, HashSet::new());
680 }
681 },
682 NetworkBridgeEvent::PeerDisconnected(peer_id) => {
683 if let Some(authority_ids) = self.connected_peers.remove(&peer_id) {
684 authority_ids.into_iter().for_each(|a| {
685 self.connected_authorities.remove(&a);
686 });
687 }
688 },
689 NetworkBridgeEvent::UpdatedAuthorityIds(_, _) => {
690 },
692 NetworkBridgeEvent::OurViewChange(_) => {},
693 NetworkBridgeEvent::PeerViewChange(_, _) => {},
694 NetworkBridgeEvent::NewGossipTopology { .. } => {},
695 NetworkBridgeEvent::PeerMessage(_, message) => {
696 match message {
698 ValidationProtocols::V3(m) => match m {},
699 }
700 },
701 }
702 }
703
704 fn check_connectivity(&mut self) {
706 let absolute_connected = self.connected_authorities.len();
707 let absolute_resolved = self.resolved_authorities.len();
708 let connected_ratio =
709 (100 * absolute_connected).checked_div(absolute_resolved).unwrap_or(100);
710 let unconnected_authorities = self
711 .resolved_authorities
712 .iter()
713 .filter(|(a, _)| !self.connected_authorities.contains_key(a));
714 if connected_ratio <= LOW_CONNECTIVITY_WARN_THRESHOLD && self.is_authority_now {
715 gum::error!(
716 target: LOG_TARGET,
717 session_index = self.last_session_index.as_ref().map(|s| *s).unwrap_or_default(),
718 "Connectivity seems low, we are only connected to {connected_ratio}% of available validators (see debug logs for details), if this persists more than a session action needs to be taken"
719 );
720 }
721 let pretty = PrettyAuthorities(unconnected_authorities);
722 gum::debug!(
723 target: LOG_TARGET,
724 ?connected_ratio,
725 ?absolute_connected,
726 ?absolute_resolved,
727 unconnected_authorities = %pretty,
728 "Connectivity Report"
729 );
730 }
731}
732
733async fn authorities_past_present_future(
735 sender: &mut impl overseer::GossipSupportSenderTrait,
736 relay_parent: Hash,
737) -> Result<Vec<AuthorityDiscoveryId>, util::Error> {
738 let authorities = util::request_authorities(relay_parent, sender).await.await??;
739 gum::debug!(
740 target: LOG_TARGET,
741 authority_count = ?authorities.len(),
742 "Determined past/present/future authorities",
743 );
744 Ok(authorities)
745}
746
747fn ensure_i_am_an_authority(
750 keystore: &KeystorePtr,
751 authorities: &[AuthorityDiscoveryId],
752) -> Result<usize, util::Error> {
753 for (i, v) in authorities.iter().enumerate() {
754 if Keystore::has_keys(&**keystore, &[(v.to_raw_vec(), AuthorityDiscoveryId::ID)]) {
755 return Ok(i)
756 }
757 }
758 Err(util::Error::NotAValidator)
759}
760
761fn remove_all_controlled(
763 keystore: &KeystorePtr,
764 authorities: &mut Vec<AuthorityDiscoveryId>,
765) -> usize {
766 let mut to_remove = Vec::new();
767 for (i, v) in authorities.iter().enumerate() {
768 if Keystore::has_keys(&**keystore, &[(v.to_raw_vec(), AuthorityDiscoveryId::ID)]) {
769 to_remove.push(i);
770 }
771 }
772
773 for i in to_remove.iter().rev().copied() {
774 authorities.remove(i);
775 }
776
777 to_remove.len()
778}
779
780async fn update_gossip_topology(
789 sender: &mut impl overseer::GossipSupportSenderTrait,
790 our_index: usize,
791 authorities: Vec<AuthorityDiscoveryId>,
792 relay_parent: Hash,
793 session_index: SessionIndex,
794) -> Result<(), util::Error> {
795 let random_seed = {
797 let (tx, rx) = oneshot::channel();
798
799 sender
802 .send_message(RuntimeApiMessage::Request(
803 relay_parent,
804 RuntimeApiRequest::CurrentBabeEpoch(tx),
805 ))
806 .await;
807
808 let randomness = rx.await??.randomness;
809 let mut subject = [0u8; 40];
810 subject[..8].copy_from_slice(b"gossipsu");
811 subject[8..].copy_from_slice(&randomness);
812 sp_crypto_hashing::blake2_256(&subject)
813 };
814
815 let (shuffled_indices, canonical_shuffling) = {
817 let mut rng: ChaCha20Rng = SeedableRng::from_seed(random_seed);
818 let len = authorities.len();
819 let mut shuffled_indices = vec![0; len];
820 let mut canonical_shuffling: Vec<_> = authorities
821 .iter()
822 .enumerate()
823 .map(|(i, a)| (a.clone(), ValidatorIndex(i as _)))
824 .collect();
825
826 fisher_yates_shuffle(&mut rng, &mut canonical_shuffling[..]);
827 for (i, (_, validator_index)) in canonical_shuffling.iter().enumerate() {
828 shuffled_indices[validator_index.0 as usize] = i;
829 }
830
831 (shuffled_indices, canonical_shuffling)
832 };
833
834 sender
835 .send_message(NetworkBridgeRxMessage::NewGossipTopology {
836 session: session_index,
837 local_index: Some(ValidatorIndex(our_index as _)),
838 canonical_shuffling,
839 shuffled_indices,
840 })
841 .await;
842
843 Ok(())
844}
845
846fn fisher_yates_shuffle<T, R: Rng + ?Sized>(rng: &mut R, items: &mut [T]) {
849 for i in (1..items.len()).rev() {
850 let index = rng.gen_range(0u32..(i as u32 + 1));
852 items.swap(i, index as usize);
853 }
854}
855
856#[overseer::subsystem(GossipSupport, error = SubsystemError, prefix = self::overseer)]
857impl<Context, AD> GossipSupport<AD>
858where
859 AD: AuthorityDiscovery + Clone,
860{
861 fn start(self, ctx: Context) -> SpawnedSubsystem {
862 let future = self.run(ctx).map(|_| Ok(())).boxed();
863
864 SpawnedSubsystem { name: "gossip-support-subsystem", future }
865 }
866}
867
868struct PrettyAuthorities<I>(I);
870
871impl<'a, I> fmt::Display for PrettyAuthorities<I>
872where
873 I: Iterator<Item = (&'a AuthorityDiscoveryId, &'a HashSet<Multiaddr>)> + Clone,
874{
875 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
876 let mut authorities = self.0.clone().peekable();
877 if authorities.peek().is_none() {
878 write!(f, "None")?;
879 } else {
880 write!(f, "\n")?;
881 }
882 for (authority, addrs) in authorities {
883 write!(f, "{}:\n", authority)?;
884 for addr in addrs {
885 write!(f, " {}\n", addr)?;
886 }
887 write!(f, "\n")?;
888 }
889 Ok(())
890 }
891}