1use std::{
35 collections::HashSet,
36 path::PathBuf,
37 sync::Arc,
38 time::{Duration, Instant},
39};
40
41use futures_timer::Delay;
42
43use sc_authority_discovery::AuthorityDiscovery;
44use sc_network::{service::traits::NetworkService, DhtEvent, Multiaddr, PeerId, ProtocolName};
45use sc_service::SpawnTaskHandle;
46use sp_api::{ApiExt, ProvideRuntimeApi};
47use sp_authority_discovery::{AuthorityDiscoveryApi, AuthorityId};
48use sp_blockchain::HeaderBackend;
49use sp_core::crypto::key_types;
50use sp_keystore::KeystorePtr;
51use sp_runtime::traits::Block as BlockT;
52
53use sc_network_sync::{block_announces_protocol_name, SyncingService};
54
55const LOG_TARGET: &str = "collator-discovery";
56
57const TRY_RERESOLVE_AUTHORITIES: Duration = Duration::from_secs(30);
59
60const MAX_ADDRS_PER_AUTHORITY: usize = 4;
63
64const LOW_CONNECTIVITY_WARN_THRESHOLD_PCT: usize = 85;
66const LOW_CONNECTIVITY_WARN_DELAY: Duration = Duration::from_secs(600);
67
68pub struct CollatorDiscoveryConfig {
69 pub max_reserved: usize,
70 pub protocol: ProtocolName,
71}
72
73pub struct StartCollatorDiscoveryParams<Block: BlockT, Client, AD, NetEventStream> {
75 pub max_reserved: usize,
79 pub client: Arc<Client>,
80 pub authority_discovery: Arc<AD>,
82 pub network: Arc<dyn NetworkService>,
83 pub sync_service: Arc<SyncingService<Block>>,
84 pub network_event_stream: NetEventStream,
86 pub keystore: KeystorePtr,
89 pub genesis_hash: Block::Hash,
90 pub fork_id: Option<String>,
91 pub publish_non_global_ips: bool,
93 pub public_addresses: Vec<Multiaddr>,
94 pub persisted_cache_directory: Option<PathBuf>,
95 pub prometheus_registry: Option<prometheus_endpoint::Registry>,
96 pub spawn_handle: SpawnTaskHandle,
97}
98
99pub fn start_collator_discovery<Block, Client, AD, NetEventStream>(
103 params: StartCollatorDiscoveryParams<Block, Client, AD, NetEventStream>,
104) -> Result<(), prometheus_endpoint::PrometheusError>
105where
106 Block: BlockT + Unpin + 'static,
107 Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + Send + Sync + 'static,
108 Client::Api: ApiExt<Block>,
109 AD: AuthorityDiscovery<Block> + Send + Sync + 'static,
110 NetEventStream: futures::Stream<Item = sc_network::Event> + Send + Unpin + 'static,
111{
112 let StartCollatorDiscoveryParams {
113 max_reserved,
114 client,
115 authority_discovery,
116 network,
117 sync_service,
118 network_event_stream,
119 keystore,
120 genesis_hash,
121 fork_id,
122 publish_non_global_ips,
123 public_addresses,
124 persisted_cache_directory,
125 prometheus_registry,
126 spawn_handle,
127 } = params;
128
129 let protocol: ProtocolName =
130 block_announces_protocol_name(genesis_hash, fork_id.as_deref()).into();
131
132 use futures::StreamExt;
133 let dht_event_stream = network_event_stream.filter_map(|e| async move {
134 match e {
135 sc_network::Event::Dht(e) => Some(e),
136 _ => None,
137 }
138 });
139
140 spawn_collator_discovery_tasks::<Block, _, _, _>(
141 CollatorDiscoveryConfig { max_reserved, protocol },
142 client,
143 authority_discovery,
144 network,
145 sync_service,
146 Box::pin(dht_event_stream),
147 keystore,
148 publish_non_global_ips,
149 public_addresses,
150 persisted_cache_directory,
151 prometheus_registry,
152 spawn_handle,
153 )
154}
155
156fn spawn_collator_discovery_tasks<Block, Client, AD, DhtStream>(
158 config: CollatorDiscoveryConfig,
159 client: Arc<Client>,
160 authority_discovery: Arc<AD>,
161 network: Arc<dyn NetworkService>,
162 sync_service: Arc<SyncingService<Block>>,
163 dht_event_stream: DhtStream,
164 keystore: KeystorePtr,
165 publish_non_global_ips: bool,
166 public_addresses: Vec<Multiaddr>,
167 persisted_cache_directory: Option<PathBuf>,
168 prometheus_registry: Option<prometheus_endpoint::Registry>,
169 spawn_handle: SpawnTaskHandle,
170) -> Result<(), prometheus_endpoint::PrometheusError>
171where
172 Block: BlockT + Unpin + 'static,
173 Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + Send + Sync + 'static,
174 Client::Api: ApiExt<Block>,
175 AD: AuthorityDiscovery<Block> + Send + Sync + 'static,
176 DhtStream: futures::Stream<Item = DhtEvent> + Send + Unpin + 'static,
177{
178 let metrics = prometheus_registry.as_ref().map(Metrics::register).transpose()?;
179
180 let (worker, authority_discovery_service) =
181 sc_authority_discovery::new_worker_and_service_with_config(
182 sc_authority_discovery::WorkerConfig {
183 publish_non_global_ips,
184 public_addresses,
185 strict_record_validation: true,
186 persisted_cache_directory,
187 ..Default::default()
188 },
189 authority_discovery.clone(),
190 Arc::new(network.clone()),
191 dht_event_stream,
192 sc_authority_discovery::Role::PublishAndDiscover(keystore.clone()),
193 prometheus_registry,
194 spawn_handle.clone(),
195 );
196
197 spawn_handle.spawn(
200 "para-authority-discovery-worker",
201 Some("authority-discovery"),
202 worker.run(),
203 );
204
205 log::info!(target: LOG_TARGET, "Starting collator discovery");
206
207 spawn_handle.spawn(
208 "collator-discovery",
209 Some("collator-discovery"),
210 discovery_refresh_loop::<Block, Client, AD>(
211 config,
212 client,
213 authority_discovery,
214 network,
215 sync_service,
216 authority_discovery_service,
217 keystore,
218 metrics,
219 ),
220 );
221
222 Ok(())
223}
224
225async fn discovery_refresh_loop<Block, Client, AD>(
227 config: CollatorDiscoveryConfig,
228 client: Arc<Client>,
229 authority_discovery: Arc<AD>,
230 network: Arc<dyn NetworkService>,
231 sync_service: Arc<SyncingService<Block>>,
232 mut authority_discovery_service: sc_authority_discovery::Service,
233 keystore: KeystorePtr,
234 metrics: Option<Metrics>,
235) where
236 Block: BlockT,
237 Client: HeaderBackend<Block> + ProvideRuntimeApi<Block> + Send + Sync + 'static,
238 Client::Api: ApiExt<Block>,
239 AD: AuthorityDiscovery<Block> + Send + Sync + 'static,
240{
241 let CollatorDiscoveryConfig { max_reserved, protocol } = config;
242
243 let local_peer_id = network.local_peer_id();
244 let mut state = LoopState::new();
245
246 loop {
248 let at = client.info().finalized_hash;
249 let ad_enabled = client
250 .runtime_api()
251 .has_api::<dyn AuthorityDiscoveryApi<Block>>(at)
252 .unwrap_or(false);
253 log::trace!(
254 target: LOG_TARGET,
255 "AuthorityDiscoveryApi at {at:?}: {}",
256 if ad_enabled { "enabled" } else { "disabled" },
257 );
258 if ad_enabled {
259 break;
260 }
261 Delay::new(TRY_RERESOLVE_AUTHORITIES).await;
262 }
263
264 loop {
266 let at = client.info().finalized_hash;
269 let local_pub_keys: HashSet<AuthorityId> = keystore
270 .sr25519_public_keys(key_types::AUTHORITY_DISCOVERY)
271 .into_iter()
272 .map(AuthorityId::from)
273 .collect();
274 update_parachain_authorities(
275 &*authority_discovery,
276 &*network,
277 &sync_service,
278 &mut authority_discovery_service,
279 &local_pub_keys,
280 local_peer_id,
281 max_reserved,
282 &protocol,
283 &mut state,
284 metrics.as_ref(),
285 at,
286 )
287 .await;
288 Delay::new(TRY_RERESOLVE_AUTHORITIES).await;
289 }
290}
291
292struct LoopState {
294 last_authorities: Option<HashSet<AuthorityId>>,
295 last_addrs: Option<HashSet<Multiaddr>>,
296 low_connectivity_since: Option<Instant>,
298}
299
300impl LoopState {
301 fn new() -> Self {
302 Self { last_authorities: None, last_addrs: None, low_connectivity_since: None }
303 }
304}
305
306fn compute_reserved_diff(
312 current: &HashSet<Multiaddr>,
313 previous: Option<&HashSet<Multiaddr>>,
314) -> (HashSet<Multiaddr>, Vec<PeerId>) {
315 let to_add = match previous {
316 Some(prev) => current.difference(prev).cloned().collect(),
317 None => current.clone(),
318 };
319 let to_remove = match previous {
320 Some(prev) => prev.difference(current).filter_map(PeerId::try_from_multiaddr).collect(),
321 None => Vec::new(),
322 };
323 (to_add, to_remove)
324}
325
326fn select_ring_neighbors(
338 authorities: Vec<AuthorityId>,
339 local_pub_keys: &HashSet<AuthorityId>,
340 max_reserved: usize,
341) -> HashSet<AuthorityId> {
342 let n = authorities.len();
343 if n == 0 || max_reserved == 0 {
344 return HashSet::new();
345 }
346 let Some(local_idx) = authorities.iter().position(|id| local_pub_keys.contains(id)) else {
347 return HashSet::new();
348 };
349
350 let half = max_reserved / 2;
351 let right = authorities.iter().cycle().skip(local_idx + 1).take(half);
352 let left = authorities.iter().cycle().skip(local_idx + n.saturating_sub(half)).take(half);
353 right.chain(left).filter(|id| !local_pub_keys.contains(id)).cloned().collect()
354}
355
356async fn update_parachain_authorities<Block, AD>(
359 authority_discovery: &AD,
360 network: &dyn NetworkService,
361 sync_service: &SyncingService<Block>,
362 authority_discovery_service: &mut sc_authority_discovery::Service,
363 local_pub_keys: &HashSet<AuthorityId>,
364 local_peer_id: PeerId,
365 max_reserved: usize,
366 protocol: &ProtocolName,
367 state: &mut LoopState,
368 metrics: Option<&Metrics>,
369 at: Block::Hash,
370) where
371 Block: BlockT,
372 AD: AuthorityDiscovery<Block> + Send + Sync + 'static,
373{
374 let authorities = match authority_discovery.authorities(at).await {
375 Ok(a) => a,
376 Err(e) => {
377 log::warn!(target: LOG_TARGET, "Failed to fetch parachain authorities at {at:?}: {e}");
378 return;
379 },
380 };
381
382 let selected = select_ring_neighbors(authorities, local_pub_keys, max_reserved);
383 let target_count = selected.len();
384 let (addrs, unresolved) =
385 resolve_authority_addresses(&selected, authority_discovery_service, local_peer_id).await;
386 let resolved = target_count.saturating_sub(unresolved);
387
388 if let Some(m) = metrics {
389 m.target_authorities.set(target_count as u64);
390 m.unresolved_authorities.set(unresolved as u64);
391 m.resolved_peers.set(addrs.len() as u64);
392 }
393
394 log_low_connectivity_if_stuck(target_count, resolved, &mut state.low_connectivity_since);
395
396 if state.last_authorities.as_ref() == Some(&selected) &&
398 state.last_addrs.as_ref() == Some(&addrs)
399 {
400 log::trace!(target: LOG_TARGET, "No-op refresh at {at:?}");
401 return;
402 }
403
404 let (to_add, to_remove) = compute_reserved_diff(&addrs, state.last_addrs.as_ref());
406
407 let peer_ids: HashSet<PeerId> = addrs.iter().filter_map(PeerId::try_from_multiaddr).collect();
408 log::debug!(
409 target: LOG_TARGET,
410 "Refreshing reserved peers at {at:?}: target={target_count} unresolved={unresolved} multiaddrs={} peer_ids={} (+{} -{})",
411 addrs.len(),
412 peer_ids.len(),
413 to_add.len(),
414 to_remove.len(),
415 );
416
417 if !to_add.is_empty() {
418 if let Err(e) = network.add_peers_to_reserved_set(protocol.clone(), to_add) {
419 log::warn!(
420 target: LOG_TARGET,
421 "add_peers_to_reserved_set failed at {at:?}: {e}; will retry on next tick",
422 );
423 return;
424 }
425 }
426
427 if !to_remove.is_empty() {
428 if let Err(e) = network.remove_peers_from_reserved_set(protocol.clone(), to_remove) {
429 log::warn!(
430 target: LOG_TARGET,
431 "remove_peers_from_reserved_set failed at {at:?}: {e}; will retry on next tick",
432 );
433 return;
434 }
435 }
436
437 sync_service.set_no_slot_peers(peer_ids);
439 state.last_authorities = Some(selected);
440 state.last_addrs = Some(addrs);
441}
442
443async fn resolve_authority_addresses(
448 authorities: &HashSet<AuthorityId>,
449 service: &mut sc_authority_discovery::Service,
450 local_peer_id: PeerId,
451) -> (HashSet<Multiaddr>, usize) {
452 let mut addrs: HashSet<Multiaddr> = HashSet::new();
453 let mut unresolved = 0;
454
455 for id in authorities {
456 let Some(raw) = service.get_addresses_by_authority_id(id.clone()).await else {
457 unresolved += 1;
458 log::debug!(target: LOG_TARGET, "Couldn't resolve addresses of authority: {id:?}");
459 continue;
460 };
461
462 let original_len = raw.len();
463 let filtered: Vec<Multiaddr> = raw
464 .into_iter()
465 .filter(|m| PeerId::try_from_multiaddr(m).map_or(true, |pid| pid != local_peer_id))
466 .take(MAX_ADDRS_PER_AUTHORITY)
467 .collect();
468
469 if original_len > MAX_ADDRS_PER_AUTHORITY {
470 log::debug!(
471 target: LOG_TARGET,
472 "Capped multiaddrs for authority {id:?}: {original_len} -> {MAX_ADDRS_PER_AUTHORITY}",
473 );
474 }
475 log::debug!(target: LOG_TARGET, "Resolved authority {id:?}: {} multiaddr(s)", filtered.len());
476 addrs.extend(filtered);
477 }
478
479 (addrs, unresolved)
480}
481
482fn log_low_connectivity_if_stuck(target: usize, resolved: usize, since: &mut Option<Instant>) {
483 if target == 0 {
484 *since = None;
485 return;
486 }
487 let pct = (resolved * 100) / target;
488 if pct >= LOW_CONNECTIVITY_WARN_THRESHOLD_PCT {
489 *since = None;
490 return;
491 }
492 match *since {
493 Some(t) if t.elapsed() >= LOW_CONNECTIVITY_WARN_DELAY => {
494 log::warn!(
495 target: LOG_TARGET,
496 "Collator discovery: peer connectivity has been under {}% for more than {:?} \
497 ({resolved}/{target} authorities resolved). Check authority-discovery \
498 DHT reachability.",
499 LOW_CONNECTIVITY_WARN_THRESHOLD_PCT, LOW_CONNECTIVITY_WARN_DELAY,
500 );
501 *since = Some(Instant::now());
503 },
504 Some(_) => {},
505 None => *since = Some(Instant::now()),
506 }
507}
508
509#[derive(Clone)]
511struct Metrics {
512 target_authorities: prometheus_endpoint::Gauge<prometheus_endpoint::U64>,
513 unresolved_authorities: prometheus_endpoint::Gauge<prometheus_endpoint::U64>,
514 resolved_peers: prometheus_endpoint::Gauge<prometheus_endpoint::U64>,
515}
516
517impl Metrics {
518 fn register(
519 registry: &prometheus_endpoint::Registry,
520 ) -> Result<Self, prometheus_endpoint::PrometheusError> {
521 use prometheus_endpoint::{register, Gauge, Opts};
522 Ok(Self {
523 target_authorities: register(
524 Gauge::with_opts(Opts::new(
525 "collator_discovery_target_authorities",
526 "Number of parachain authorities currently targeted for reservation.",
527 ))?,
528 registry,
529 )?,
530 unresolved_authorities: register(
531 Gauge::with_opts(Opts::new(
532 "collator_discovery_unresolved_authorities",
533 "Number of targeted authorities we couldn't resolve a multiaddr for.",
534 ))?,
535 registry,
536 )?,
537 resolved_peers: register(
538 Gauge::with_opts(Opts::new(
539 "collator_discovery_resolved_peers",
540 "Number of multiaddrs pushed to the collator-sync reserved set.",
541 ))?,
542 registry,
543 )?,
544 })
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551 use sp_core::sr25519;
552
553 fn authority_id(seed_byte: u8) -> AuthorityId {
554 AuthorityId::from(sr25519::Public::from_raw([seed_byte; 32]))
555 }
556
557 fn ring(n: u8) -> Vec<AuthorityId> {
559 (0..n).map(authority_id).collect()
560 }
561
562 fn local_set(seed: u8) -> HashSet<AuthorityId> {
563 let mut s = HashSet::new();
564 s.insert(authority_id(seed));
565 s
566 }
567
568 #[test]
569 fn ring_picks_immediate_neighbors_with_min_budget() {
570 let selected = select_ring_neighbors(ring(5), &local_set(2), 2);
572 assert_eq!(selected, HashSet::from([authority_id(3), authority_id(1)]));
573 }
574
575 #[test]
576 fn ring_wraps_around_at_the_ends() {
577 let selected = select_ring_neighbors(ring(5), &local_set(0), 2);
579 assert_eq!(selected, HashSet::from([authority_id(1), authority_id(4)]));
580 }
581
582 #[test]
583 fn ring_grows_balanced_on_both_sides() {
584 let selected = select_ring_neighbors(ring(5), &local_set(2), 4);
586 assert_eq!(
587 selected,
588 HashSet::from([authority_id(3), authority_id(1), authority_id(4), authority_id(0)]),
589 );
590 }
591
592 #[test]
593 fn ring_odd_budget_leaves_one_slot_unused_to_stay_symmetric() {
594 let selected = select_ring_neighbors(ring(5), &local_set(2), 3);
596 assert_eq!(selected, HashSet::from([authority_id(3), authority_id(1)]));
597 }
598
599 #[test]
600 fn ring_becomes_full_mesh_when_budget_covers_everyone() {
601 let selected = select_ring_neighbors(ring(5), &local_set(2), 100);
602 let expected: HashSet<_> = [0u8, 1, 3, 4].into_iter().map(authority_id).collect();
603 assert_eq!(selected, expected);
604 }
605
606 #[test]
607 fn ring_excludes_self() {
608 let selected = select_ring_neighbors(ring(4), &local_set(1), 100);
609 assert!(!selected.contains(&authority_id(1)));
610 }
611
612 #[test]
613 fn ring_handles_even_antipode_without_duplicates() {
614 let selected = select_ring_neighbors(ring(4), &local_set(0), 100);
617 assert_eq!(selected, HashSet::from([authority_id(1), authority_id(2), authority_id(3)]),);
618 }
619
620 #[test]
621 fn ring_alone_selects_nothing() {
622 let selected = select_ring_neighbors(ring(1), &local_set(0), 100);
623 assert!(selected.is_empty());
624 }
625
626 #[test]
627 fn ring_empty_when_not_an_authority() {
628 let selected = select_ring_neighbors(ring(5), &local_set(99), 3);
630 assert!(selected.is_empty());
631 }
632
633 #[test]
634 fn ring_neighbor_relationships_are_mutual() {
635 let n = 7u8;
637 let budget = 4;
638 let pick = |me: u8| -> HashSet<u8> {
639 select_ring_neighbors(ring(n), &local_set(me), budget)
640 .into_iter()
641 .map(|id| AsRef::<[u8]>::as_ref(&id)[0])
642 .collect()
643 };
644 for me in 0..n {
645 for peer in pick(me) {
646 assert!(
647 pick(peer).contains(&me),
648 "{me} reserved {peer} but {peer} did not reserve {me} back",
649 );
650 }
651 }
652 }
653
654 #[test]
655 fn low_connectivity_starts_quiet() {
656 let mut since = None;
657 log_low_connectivity_if_stuck(4, 4, &mut since);
659 assert!(since.is_none());
660 }
661
662 #[test]
663 fn low_connectivity_remembers_drop_below_threshold() {
664 let mut since = None;
665 log_low_connectivity_if_stuck(4, 1, &mut since);
667 assert!(since.is_some());
668 }
669
670 #[test]
671 fn low_connectivity_clears_on_recovery() {
672 let mut since = Some(Instant::now());
673 log_low_connectivity_if_stuck(4, 4, &mut since);
674 assert!(since.is_none());
675 }
676
677 #[test]
678 fn low_connectivity_noop_on_zero_target() {
679 let mut since = Some(Instant::now());
680 log_low_connectivity_if_stuck(0, 0, &mut since);
681 assert!(since.is_none());
682 }
683
684 fn ma(seed: u8) -> Multiaddr {
686 use sc_network::{multiaddr::Protocol, Keypair};
687 let kp = Keypair::ed25519_from_bytes([seed; 32]).expect("32-byte seed; qed");
688 let pid: PeerId = kp.public().to_peer_id().into();
689 Multiaddr::empty()
690 .with(Protocol::Memory(seed as u64))
691 .with(Protocol::P2p(pid.into()))
692 }
693
694 fn set_of(ms: impl IntoIterator<Item = Multiaddr>) -> HashSet<Multiaddr> {
695 ms.into_iter().collect()
696 }
697
698 fn pid_of(m: &Multiaddr) -> PeerId {
699 PeerId::try_from_multiaddr(m).expect("multiaddr from ma() has /p2p/")
700 }
701
702 #[test]
703 fn diff_initial_tick_adds_everything_no_removals() {
704 let current = set_of([ma(1), ma(2)]);
705 let (to_add, to_remove) = compute_reserved_diff(¤t, None);
706 assert_eq!(to_add, current);
707 assert!(to_remove.is_empty(), "no previous state => nothing to remove");
708 }
709
710 #[test]
711 fn diff_steady_state_is_noop() {
712 let current = set_of([ma(1), ma(2)]);
713 let previous = current.clone();
714 let (to_add, to_remove) = compute_reserved_diff(¤t, Some(&previous));
715 assert!(to_add.is_empty());
716 assert!(to_remove.is_empty());
717 }
718
719 #[test]
720 fn diff_addition_only() {
721 let previous = set_of([ma(1)]);
722 let current = set_of([ma(1), ma(2)]);
723 let (to_add, to_remove) = compute_reserved_diff(¤t, Some(&previous));
724 assert_eq!(to_add, set_of([ma(2)]));
725 assert!(to_remove.is_empty());
726 }
727
728 #[test]
729 fn diff_removal_only() {
730 let previous = set_of([ma(1), ma(2)]);
731 let current = set_of([ma(1)]);
732 let (to_add, to_remove) = compute_reserved_diff(¤t, Some(&previous));
733 assert!(to_add.is_empty());
734 assert_eq!(to_remove, vec![pid_of(&ma(2))]);
735 }
736
737 #[test]
738 fn diff_mixed_add_and_remove() {
739 let previous = set_of([ma(1), ma(2)]);
740 let current = set_of([ma(1), ma(3)]);
741 let (to_add, to_remove) = compute_reserved_diff(¤t, Some(&previous));
742 assert_eq!(to_add, set_of([ma(3)]));
743 assert_eq!(to_remove, vec![pid_of(&ma(2))]);
744 }
745
746 #[test]
747 fn diff_back_to_empty_removes_everything_previously_added() {
748 let previous = set_of([ma(1), ma(2)]);
749 let current = HashSet::new();
750 let (to_add, to_remove) = compute_reserved_diff(¤t, Some(&previous));
751 assert!(to_add.is_empty());
752 let removed: HashSet<_> = to_remove.into_iter().collect();
753 assert_eq!(removed, set_of([ma(1), ma(2)]).iter().map(pid_of).collect::<HashSet<_>>());
754 }
755
756 #[test]
757 fn diff_empty_to_empty_is_total_noop() {
758 let current = HashSet::new();
760 let (to_add, to_remove) = compute_reserved_diff(¤t, None);
761 assert!(to_add.is_empty());
762 assert!(to_remove.is_empty());
763 }
764}