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 pezsc_network::{config::parse_addr, Multiaddr};
40use pezsp_application_crypto::{AppCrypto, ByteArray};
41use pezsp_keystore::{Keystore, KeystorePtr};
42
43use pezkuwi_node_network_protocol::{
44 authority_discovery::AuthorityDiscovery, peer_set::PeerSet, GossipSupportNetworkMessage,
45 PeerId, ValidationProtocols,
46};
47use pezkuwi_node_subsystem::{
48 messages::{
49 ChainApiMessage, GossipSupportMessage, NetworkBridgeEvent, NetworkBridgeRxMessage,
50 NetworkBridgeTxMessage, RuntimeApiMessage, RuntimeApiRequest,
51 },
52 overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError,
53};
54use pezkuwi_node_subsystem_util as util;
55use pezkuwi_primitives::{AuthorityDiscoveryId, Hash, SessionIndex, SessionInfo, ValidatorIndex};
56
57#[cfg(test)]
58mod tests;
59
60mod metrics;
61
62use metrics::Metrics;
63
64const LOG_TARGET: &str = "teyrchain::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_teyrchain_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 },
230 FromOrchestra::Signal(OverseerSignal::Conclude) => return self,
231 }
232 }
233 }
234
235 async fn handle_active_leaves(
238 &mut self,
239 sender: &mut impl overseer::GossipSupportSenderTrait,
240 leaves: impl Iterator<Item = Hash>,
241 ) -> Result<(), util::Error> {
242 for leaf in leaves {
243 let current_index = util::request_session_index_for_child(leaf, sender).await.await??;
244 let since_failure = self.last_failure.map(|i| i.elapsed()).unwrap_or_default();
245 let since_last_reconnect =
246 self.last_connection_request.map(|i| i.elapsed()).unwrap_or_default();
247
248 let force_request = since_failure >= BACKOFF_DURATION;
249 let re_resolve_authorities = since_last_reconnect >= TRY_RERESOLVE_AUTHORITIES;
250 let leaf_session = Some((current_index, leaf));
251 let maybe_new_session = match self.last_session_index {
252 Some(i) if current_index <= i => None,
253 _ => leaf_session,
254 };
255
256 let maybe_issue_connection = if force_request || re_resolve_authorities {
257 leaf_session
258 } else {
259 maybe_new_session
260 };
261
262 if let Some((session_index, relay_parent)) = maybe_issue_connection {
263 let session_info =
264 util::request_session_info(leaf, session_index, sender).await.await??;
265
266 let session_info = match session_info {
267 Some(s) => s,
268 None => {
269 gum::warn!(
270 relay_parent = ?leaf,
271 session_index = self.last_session_index,
272 "Failed to get session info.",
273 );
274
275 continue;
276 },
277 };
278
279 let is_new_session = maybe_new_session.is_some();
282 if is_new_session {
283 gum::debug!(
284 target: LOG_TARGET,
285 %session_index,
286 "New session detected",
287 );
288 self.last_session_index = Some(session_index);
289 self.is_authority_now =
290 ensure_i_am_an_authority(&self.keystore, &session_info.discovery_keys)
291 .is_ok();
292 }
293
294 {
306 let mut connections = authorities_past_present_future(sender, leaf).await?;
307 self.last_connection_request = Some(Instant::now());
308 let connections =
311 if remove_all_controlled(&self.keystore, &mut connections) != 0 {
312 connections
313 } else {
314 Vec::new()
317 };
318
319 if force_request || is_new_session {
320 self.issue_connection_request(sender, connections).await;
321 } else if re_resolve_authorities {
322 self.issue_connection_request_to_changed(sender, connections).await;
323 }
324 }
325
326 if is_new_session {
327 if let Err(err) = self
328 .build_topology_for_last_finalized_if_needed(sender, session_index)
329 .await
330 {
331 gum::warn!(
332 target: LOG_TARGET,
333 "Failed to build topology for last finalized session: {:?}",
334 err
335 );
336 }
337
338 let our_index = self.get_key_index_and_update_metrics(&session_info)?;
340 update_gossip_topology(
341 sender,
342 our_index,
343 session_info.discovery_keys.clone(),
344 relay_parent,
345 session_index,
346 )
347 .await?;
348 }
349 self.update_authority_ids(sender, session_info.discovery_keys).await;
352 }
353 }
354 Ok(())
355 }
356
357 async fn build_topology_for_last_finalized_if_needed(
365 &mut self,
366 sender: &mut impl overseer::GossipSupportSenderTrait,
367 current_session_index: u32,
368 ) -> Result<(), util::Error> {
369 self.min_known_session = self.min_known_session.min(current_session_index);
370
371 if self
372 .finalized_needed_session
373 .map(|oldest_needed_session| oldest_needed_session < self.min_known_session)
374 .unwrap_or(true)
375 {
376 let (tx, rx) = oneshot::channel();
377 sender.send_message(ChainApiMessage::FinalizedBlockNumber(tx)).await;
378 let finalized_block_number = match rx.await? {
379 Ok(block_number) => block_number,
380 _ => return Ok(()),
381 };
382
383 let (tx, rx) = oneshot::channel();
384 sender
385 .send_message(ChainApiMessage::FinalizedBlockHash(finalized_block_number, tx))
386 .await;
387
388 let finalized_block_hash = match rx.await? {
389 Ok(Some(block_hash)) => block_hash,
390 _ => return Ok(()),
391 };
392
393 let finalized_session_index =
394 util::request_session_index_for_child(finalized_block_hash, sender)
395 .await
396 .await??;
397
398 if finalized_session_index < self.min_known_session
399 && Some(finalized_session_index) != self.finalized_needed_session
400 {
401 gum::debug!(
402 target: LOG_TARGET,
403 ?finalized_block_hash,
404 ?finalized_block_number,
405 ?finalized_session_index,
406 "Building topology for finalized block session",
407 );
408
409 let finalized_session_info = match util::request_session_info(
410 finalized_block_hash,
411 finalized_session_index,
412 sender,
413 )
414 .await
415 .await??
416 {
417 Some(session_info) => session_info,
418 _ => return Ok(()),
419 };
420
421 let our_index = self.get_key_index_and_update_metrics(&finalized_session_info)?;
422 update_gossip_topology(
423 sender,
424 our_index,
425 finalized_session_info.discovery_keys.clone(),
426 finalized_block_hash,
427 finalized_session_index,
428 )
429 .await?;
430 }
431 self.finalized_needed_session = Some(finalized_session_index);
432 }
433 Ok(())
434 }
435
436 fn get_key_index_and_update_metrics(
440 &mut self,
441 session_info: &SessionInfo,
442 ) -> Result<usize, util::Error> {
443 let authority_check_result =
444 ensure_i_am_an_authority(&self.keystore, &session_info.discovery_keys);
445
446 match authority_check_result.as_ref() {
447 Ok(index) => {
448 gum::trace!(target: LOG_TARGET, "We are now an authority",);
449 self.metrics.on_is_authority();
450
451 let teyrchain_validators_this_session = session_info.validators.len();
453
454 if *index < teyrchain_validators_this_session {
458 gum::trace!(target: LOG_TARGET, "We are now a teyrchain validator",);
459 self.metrics.on_is_teyrchain_validator();
460 } else {
461 gum::trace!(target: LOG_TARGET, "We are no longer a teyrchain validator",);
462 self.metrics.on_is_not_teyrchain_validator();
463 }
464 },
465 Err(util::Error::NotAValidator) => {
466 gum::trace!(target: LOG_TARGET, "We are no longer an authority",);
467 self.metrics.on_is_not_authority();
468 self.metrics.on_is_not_teyrchain_validator();
469 },
470 Err(_) => {},
472 };
473
474 authority_check_result
475 }
476
477 async fn resolve_authorities(
478 &mut self,
479 authorities: Vec<AuthorityDiscoveryId>,
480 ) -> (Vec<HashSet<Multiaddr>>, HashMap<AuthorityDiscoveryId, HashSet<Multiaddr>>, usize) {
481 let mut validator_addrs = Vec::with_capacity(authorities.len());
482 let mut resolved = HashMap::with_capacity(authorities.len());
483 let mut failures = 0;
484
485 for authority in authorities {
486 if let Some(addrs) =
487 self.authority_discovery.get_addresses_by_authority_id(authority.clone()).await
488 {
489 validator_addrs.push(addrs.clone());
490 resolved.insert(authority, addrs);
491 } else {
492 failures += 1;
493 gum::debug!(
494 target: LOG_TARGET,
495 "Couldn't resolve addresses of authority: {:?}",
496 authority
497 );
498 }
499 }
500 (validator_addrs, resolved, failures)
501 }
502
503 async fn issue_connection_request_to_changed<Sender>(
504 &mut self,
505 sender: &mut Sender,
506 authorities: Vec<AuthorityDiscoveryId>,
507 ) where
508 Sender: overseer::GossipSupportSenderTrait,
509 {
510 let (_, resolved, _) = self.resolve_authorities(authorities).await;
511
512 let mut changed = Vec::new();
513
514 for (authority, new_addresses) in &resolved {
515 let new_peer_ids = new_addresses
516 .iter()
517 .flat_map(|addr| parse_addr(addr.clone()).ok().map(|(p, _)| p))
518 .collect::<HashSet<_>>();
519 match self.resolved_authorities.get(authority) {
520 Some(old_addresses) => {
521 let old_peer_ids = old_addresses
522 .iter()
523 .flat_map(|addr| parse_addr(addr.clone()).ok().map(|(p, _)| p))
524 .collect::<HashSet<_>>();
525 if !old_peer_ids.is_superset(&new_peer_ids) {
526 changed.push(new_addresses.clone());
527 }
528 },
529 None => changed.push(new_addresses.clone()),
530 }
531 }
532 gum::debug!(
533 target: LOG_TARGET,
534 num_changed = ?changed.len(),
535 ?changed,
536 "Issuing a connection request to changed validators"
537 );
538 if !changed.is_empty() {
539 self.resolved_authorities = resolved;
540
541 sender
542 .send_message(NetworkBridgeTxMessage::AddToResolvedValidators {
543 validator_addrs: changed,
544 peer_set: PeerSet::Validation,
545 })
546 .await;
547 }
548 }
549
550 async fn issue_connection_request<Sender>(
551 &mut self,
552 sender: &mut Sender,
553 authorities: Vec<AuthorityDiscoveryId>,
554 ) where
555 Sender: overseer::GossipSupportSenderTrait,
556 {
557 let num = authorities.len();
558
559 let (validator_addrs, resolved, failures) = self.resolve_authorities(authorities).await;
560
561 self.resolved_authorities = resolved;
562 gum::debug!(target: LOG_TARGET, %num, "Issuing a connection request");
563
564 sender
565 .send_message(NetworkBridgeTxMessage::ConnectToResolvedValidators {
566 validator_addrs,
567 peer_set: PeerSet::Validation,
568 })
569 .await;
570
571 if num != 0 && 3 * failures >= num {
574 let timestamp = Instant::now();
575 match self.failure_start {
576 None => self.failure_start = Some(timestamp),
577 Some(first) if first.elapsed() >= LOW_CONNECTIVITY_WARN_DELAY => {
578 gum::warn!(
579 target: LOG_TARGET,
580 connected = ?(num - failures),
581 target = ?num,
582 "Low connectivity - authority lookup failed for too many validators."
583 );
584 },
585 Some(_) => {
586 gum::debug!(
587 target: LOG_TARGET,
588 connected = ?(num - failures),
589 target = ?num,
590 "Low connectivity (due to authority lookup failures) - expected on startup."
591 );
592 },
593 }
594 self.last_failure = Some(timestamp);
595 } else {
596 self.last_failure = None;
597 self.failure_start = None;
598 };
599 }
600
601 async fn update_authority_ids<Sender>(
602 &mut self,
603 sender: &mut Sender,
604 authorities: Vec<AuthorityDiscoveryId>,
605 ) where
606 Sender: overseer::GossipSupportSenderTrait,
607 {
608 let mut authority_ids: HashMap<PeerId, HashSet<AuthorityDiscoveryId>> = HashMap::new();
609 for authority in authorities {
610 let peer_ids = self
611 .authority_discovery
612 .get_addresses_by_authority_id(authority.clone())
613 .await
614 .into_iter()
615 .flat_map(|list| list.into_iter())
616 .flat_map(|addr| parse_addr(addr).ok().map(|(p, _)| p))
617 .collect::<HashSet<_>>();
618
619 gum::trace!(
620 target: LOG_TARGET,
621 ?peer_ids,
622 ?authority,
623 "Resolved to peer ids"
624 );
625
626 for p in peer_ids {
627 authority_ids.entry(p).or_default().insert(authority.clone());
628 }
629 }
630
631 for (peer_id, current) in self.connected_peers.iter_mut() {
633 if !current.is_empty() && !authority_ids.contains_key(peer_id) {
635 sender
636 .send_message(NetworkBridgeRxMessage::UpdatedAuthorityIds {
637 peer_id: *peer_id,
638 authority_ids: HashSet::new(),
639 })
640 .await;
641
642 for a in current.drain() {
643 self.connected_authorities.remove(&a);
644 }
645 }
646 }
647
648 for (peer_id, new) in authority_ids {
650 if let Some(prev) = self.connected_peers.get(&peer_id).filter(|x| x != &&new) {
652 sender
653 .send_message(NetworkBridgeRxMessage::UpdatedAuthorityIds {
654 peer_id,
655 authority_ids: new.clone(),
656 })
657 .await;
658
659 prev.iter().for_each(|a| {
660 self.connected_authorities.remove(a);
661 });
662 new.iter().for_each(|a| {
663 self.connected_authorities.insert(a.clone(), peer_id);
664 });
665
666 self.connected_peers.insert(peer_id, new);
667 }
668 }
669 }
670
671 fn handle_connect_disconnect(&mut self, ev: NetworkBridgeEvent<GossipSupportNetworkMessage>) {
672 match ev {
673 NetworkBridgeEvent::PeerConnected(peer_id, _, _, o_authority) => {
674 if let Some(authority_ids) = o_authority {
675 authority_ids.iter().for_each(|a| {
676 self.connected_authorities.insert(a.clone(), peer_id);
677 });
678 self.connected_peers.insert(peer_id, authority_ids);
679 } else {
680 self.connected_peers.insert(peer_id, HashSet::new());
681 }
682 },
683 NetworkBridgeEvent::PeerDisconnected(peer_id) => {
684 if let Some(authority_ids) = self.connected_peers.remove(&peer_id) {
685 authority_ids.into_iter().for_each(|a| {
686 self.connected_authorities.remove(&a);
687 });
688 }
689 },
690 NetworkBridgeEvent::UpdatedAuthorityIds(_, _) => {
691 },
693 NetworkBridgeEvent::OurViewChange(_) => {},
694 NetworkBridgeEvent::PeerViewChange(_, _) => {},
695 NetworkBridgeEvent::NewGossipTopology { .. } => {},
696 NetworkBridgeEvent::PeerMessage(_, message) => {
697 match message {
699 ValidationProtocols::V3(m) => match m {},
700 }
701 },
702 }
703 }
704
705 fn check_connectivity(&mut self) {
707 let absolute_connected = self.connected_authorities.len();
708 let absolute_resolved = self.resolved_authorities.len();
709 let connected_ratio =
710 (100 * absolute_connected).checked_div(absolute_resolved).unwrap_or(100);
711 let unconnected_authorities = self
712 .resolved_authorities
713 .iter()
714 .filter(|(a, _)| !self.connected_authorities.contains_key(a));
715 if connected_ratio <= LOW_CONNECTIVITY_WARN_THRESHOLD && self.is_authority_now {
716 gum::error!(
717 target: LOG_TARGET,
718 session_index = self.last_session_index.as_ref().map(|s| *s).unwrap_or_default(),
719 "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"
720 );
721 }
722 let pretty = PrettyAuthorities(unconnected_authorities);
723 gum::debug!(
724 target: LOG_TARGET,
725 ?connected_ratio,
726 ?absolute_connected,
727 ?absolute_resolved,
728 unconnected_authorities = %pretty,
729 "Connectivity Report"
730 );
731 }
732}
733
734async fn authorities_past_present_future(
736 sender: &mut impl overseer::GossipSupportSenderTrait,
737 relay_parent: Hash,
738) -> Result<Vec<AuthorityDiscoveryId>, util::Error> {
739 let authorities = util::request_authorities(relay_parent, sender).await.await??;
740 gum::debug!(
741 target: LOG_TARGET,
742 authority_count = ?authorities.len(),
743 "Determined past/present/future authorities",
744 );
745 Ok(authorities)
746}
747
748fn ensure_i_am_an_authority(
751 keystore: &KeystorePtr,
752 authorities: &[AuthorityDiscoveryId],
753) -> Result<usize, util::Error> {
754 for (i, v) in authorities.iter().enumerate() {
755 if Keystore::has_keys(&**keystore, &[(v.to_raw_vec(), AuthorityDiscoveryId::ID)]) {
756 return Ok(i);
757 }
758 }
759 Err(util::Error::NotAValidator)
760}
761
762fn remove_all_controlled(
764 keystore: &KeystorePtr,
765 authorities: &mut Vec<AuthorityDiscoveryId>,
766) -> usize {
767 let mut to_remove = Vec::new();
768 for (i, v) in authorities.iter().enumerate() {
769 if Keystore::has_keys(&**keystore, &[(v.to_raw_vec(), AuthorityDiscoveryId::ID)]) {
770 to_remove.push(i);
771 }
772 }
773
774 for i in to_remove.iter().rev().copied() {
775 authorities.remove(i);
776 }
777
778 to_remove.len()
779}
780
781async fn update_gossip_topology(
790 sender: &mut impl overseer::GossipSupportSenderTrait,
791 our_index: usize,
792 authorities: Vec<AuthorityDiscoveryId>,
793 relay_parent: Hash,
794 session_index: SessionIndex,
795) -> Result<(), util::Error> {
796 let random_seed = {
798 let (tx, rx) = oneshot::channel();
799
800 sender
803 .send_message(RuntimeApiMessage::Request(
804 relay_parent,
805 RuntimeApiRequest::CurrentBabeEpoch(tx),
806 ))
807 .await;
808
809 let randomness = rx.await??.randomness;
810 let mut subject = [0u8; 40];
811 subject[..8].copy_from_slice(b"gossipsu");
812 subject[8..].copy_from_slice(&randomness);
813 pezsp_crypto_hashing::blake2_256(&subject)
814 };
815
816 let (shuffled_indices, canonical_shuffling) = {
818 let mut rng: ChaCha20Rng = SeedableRng::from_seed(random_seed);
819 let len = authorities.len();
820 let mut shuffled_indices = vec![0; len];
821 let mut canonical_shuffling: Vec<_> = authorities
822 .iter()
823 .enumerate()
824 .map(|(i, a)| (a.clone(), ValidatorIndex(i as _)))
825 .collect();
826
827 fisher_yates_shuffle(&mut rng, &mut canonical_shuffling[..]);
828 for (i, (_, validator_index)) in canonical_shuffling.iter().enumerate() {
829 shuffled_indices[validator_index.0 as usize] = i;
830 }
831
832 (shuffled_indices, canonical_shuffling)
833 };
834
835 sender
836 .send_message(NetworkBridgeRxMessage::NewGossipTopology {
837 session: session_index,
838 local_index: Some(ValidatorIndex(our_index as _)),
839 canonical_shuffling,
840 shuffled_indices,
841 })
842 .await;
843
844 Ok(())
845}
846
847fn fisher_yates_shuffle<T, R: Rng + ?Sized>(rng: &mut R, items: &mut [T]) {
850 for i in (1..items.len()).rev() {
851 let index = rng.gen_range(0u32..(i as u32 + 1));
853 items.swap(i, index as usize);
854 }
855}
856
857#[overseer::subsystem(GossipSupport, error = SubsystemError, prefix = self::overseer)]
858impl<Context, AD> GossipSupport<AD>
859where
860 AD: AuthorityDiscovery + Clone,
861{
862 fn start(self, ctx: Context) -> SpawnedSubsystem {
863 let future = self.run(ctx).map(|_| Ok(())).boxed();
864
865 SpawnedSubsystem { name: "gossip-support-subsystem", future }
866 }
867}
868
869struct PrettyAuthorities<I>(I);
871
872impl<'a, I> fmt::Display for PrettyAuthorities<I>
873where
874 I: Iterator<Item = (&'a AuthorityDiscoveryId, &'a HashSet<Multiaddr>)> + Clone,
875{
876 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
877 let mut authorities = self.0.clone().peekable();
878 if authorities.peek().is_none() {
879 write!(f, "None")?;
880 } else {
881 write!(f, "\n")?;
882 }
883 for (authority, addrs) in authorities {
884 write!(f, "{}:\n", authority)?;
885 for addr in addrs {
886 write!(f, " {}\n", addr)?;
887 }
888 write!(f, "\n")?;
889 }
890 Ok(())
891 }
892}