referrerpolicy=no-referrer-when-downgrade

sc_network_sync/
engine.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! `SyncingEngine` is the actor responsible for syncing Substrate chain
20//! to tip and keep the blockchain up to date with network updates.
21
22use crate::{
23	block_announce_validator::{
24		BlockAnnounceValidationResult, BlockAnnounceValidator as BlockAnnounceValidatorStream,
25	},
26	pending_responses::{PendingResponses, ResponseEvent},
27	service::{
28		self,
29		syncing_service::{SyncingService, ToServiceCommand},
30	},
31	strategy::{SyncingAction, SyncingStrategy},
32	types::{BadPeer, ExtendedPeerInfo, SyncEvent},
33	LOG_TARGET,
34};
35
36use codec::{Decode, DecodeAll, Encode};
37use futures::{channel::oneshot, StreamExt};
38use log::{debug, error, trace};
39use prometheus_endpoint::{
40	register, Counter, Gauge, MetricSource, Opts, PrometheusError, Registry, SourcedGauge, U64,
41};
42use schnellru::{ByLength, LruMap};
43use tokio::time::{Interval, MissedTickBehavior};
44
45use sc_client_api::{BlockBackend, HeaderBackend, ProofProvider};
46use sc_consensus::{import_queue::ImportQueueService, IncomingBlock};
47use sc_network::{
48	config::{FullNetworkConfiguration, NotificationHandshake, ProtocolId, SetConfig},
49	peer_store::PeerStoreProvider,
50	request_responses::{OutboundFailure, RequestFailure},
51	service::{
52		traits::{Direction, NotificationConfig, NotificationEvent, ValidationResult},
53		NotificationMetrics,
54	},
55	types::ProtocolName,
56	utils::LruHashSet,
57	NetworkBackend, NotificationService, ReputationChange,
58};
59use sc_network_common::{
60	role::Roles,
61	sync::message::{BlockAnnounce, BlockAnnouncesHandshake, BlockState},
62};
63use sc_network_types::PeerId;
64use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
65use sp_blockchain::{Error as ClientError, HeaderMetadata};
66use sp_consensus::{block_validation::BlockAnnounceValidator, BlockOrigin};
67use sp_runtime::{
68	traits::{Block as BlockT, Header, NumberFor, Zero},
69	Justifications,
70};
71
72use std::{
73	collections::{HashMap, HashSet},
74	iter,
75	num::NonZeroUsize,
76	sync::{
77		atomic::{AtomicBool, AtomicUsize, Ordering},
78		Arc,
79	},
80};
81
82/// Interval at which we perform time based maintenance
83const TICK_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(1100);
84
85/// Maximum number of known block hashes to keep for a peer.
86const MAX_KNOWN_BLOCKS: usize = 1024; // ~32kb per peer + LruHashSet overhead
87
88/// Maximum allowed size for a block announce.
89const MAX_BLOCK_ANNOUNCE_SIZE: u64 = 1024 * 1024;
90
91/// Generate the block announces protocol name from the genesis hash and fork id.
92pub fn block_announces_protocol_name<Hash: AsRef<[u8]>>(
93	genesis_hash: Hash,
94	fork_id: Option<&str>,
95) -> String {
96	let genesis_hash = genesis_hash.as_ref();
97	if let Some(fork_id) = fork_id {
98		format!("/{}/{}/block-announces/1", array_bytes::bytes2hex("", genesis_hash), fork_id)
99	} else {
100		format!("/{}/block-announces/1", array_bytes::bytes2hex("", genesis_hash))
101	}
102}
103
104/// Generate the legacy block announces protocol name from chain specific protocol identifier.
105pub fn block_announces_legacy_protocol_name(protocol_id: &ProtocolId) -> String {
106	format!("/{}/block-announces/1", protocol_id.as_ref())
107}
108
109mod rep {
110	use sc_network::ReputationChange as Rep;
111	/// Peer has different genesis.
112	pub const GENESIS_MISMATCH: Rep = Rep::new_fatal("Genesis mismatch");
113	/// Peer send us a block announcement that failed at validation.
114	pub const BAD_BLOCK_ANNOUNCEMENT: Rep = Rep::new(-(1 << 12), "Bad block announcement");
115	/// Peer is on unsupported protocol version.
116	pub const BAD_PROTOCOL: Rep = Rep::new_fatal("Unsupported protocol");
117	/// Reputation change when a peer refuses a request.
118	pub const REFUSED: Rep = Rep::new(-(1 << 10), "Request refused");
119	/// Reputation change when a peer doesn't respond in time to our messages.
120	pub const TIMEOUT: Rep = Rep::new(-(1 << 10), "Request timeout");
121	/// Reputation change when a peer connection failed with IO error.
122	pub const IO: Rep = Rep::new(-(1 << 10), "IO error during request");
123}
124
125struct Metrics {
126	peers: Gauge<U64>,
127	import_queue_blocks_submitted: Counter<U64>,
128	import_queue_justifications_submitted: Counter<U64>,
129}
130
131impl Metrics {
132	fn register(
133		r: &Registry,
134		major_syncing: Arc<AtomicBool>,
135		num_connected: Arc<AtomicUsize>,
136	) -> Result<Self, PrometheusError> {
137		MajorSyncingGauge::register(r, major_syncing)?;
138		NumConnectedGauge::register(r, num_connected)?;
139		Ok(Self {
140			peers: {
141				let g = Gauge::new("substrate_sync_peers", "Number of peers we sync with")?;
142				register(g, r)?
143			},
144			import_queue_blocks_submitted: {
145				let c = Counter::new(
146					"substrate_sync_import_queue_blocks_submitted",
147					"Number of blocks submitted to the import queue.",
148				)?;
149				register(c, r)?
150			},
151			import_queue_justifications_submitted: {
152				let c = Counter::new(
153					"substrate_sync_import_queue_justifications_submitted",
154					"Number of justifications submitted to the import queue.",
155				)?;
156				register(c, r)?
157			},
158		})
159	}
160}
161
162/// The "major syncing" metric.
163#[derive(Clone)]
164pub struct MajorSyncingGauge(Arc<AtomicBool>);
165
166impl MajorSyncingGauge {
167	/// Registers the [`MajorSyncGauge`] metric whose value is
168	/// obtained from the given `AtomicBool`.
169	fn register(registry: &Registry, value: Arc<AtomicBool>) -> Result<(), PrometheusError> {
170		prometheus_endpoint::register(
171			SourcedGauge::new(
172				&Opts::new(
173					"substrate_sub_libp2p_is_major_syncing",
174					"Whether the node is performing a major sync or not.",
175				),
176				MajorSyncingGauge(value),
177			)?,
178			registry,
179		)?;
180
181		Ok(())
182	}
183}
184
185impl MetricSource for MajorSyncingGauge {
186	type N = u64;
187
188	fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
189		set(&[], self.0.load(Ordering::Relaxed) as u64);
190	}
191}
192
193/// The "number of connected peers" metric.
194#[derive(Clone)]
195struct NumConnectedGauge(Arc<AtomicUsize>);
196
197impl NumConnectedGauge {
198	/// Registers the [`NumConnectedGauge`] metric whose value is
199	/// obtained from the given `AtomicUsize`.
200	fn register(registry: &Registry, value: Arc<AtomicUsize>) -> Result<(), PrometheusError> {
201		prometheus_endpoint::register(
202			SourcedGauge::new(
203				&Opts::new("substrate_sub_libp2p_peers_count", "Number of connected peers"),
204				NumConnectedGauge(value),
205			)?,
206			registry,
207		)?;
208
209		Ok(())
210	}
211}
212
213impl MetricSource for NumConnectedGauge {
214	type N = u64;
215
216	fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
217		set(&[], self.0.load(Ordering::Relaxed) as u64);
218	}
219}
220
221/// Peer information
222#[derive(Debug)]
223pub struct Peer<B: BlockT> {
224	pub info: ExtendedPeerInfo<B>,
225	/// Holds a set of blocks known to this peer.
226	pub known_blocks: LruHashSet<B::Hash>,
227	/// Is the peer inbound.
228	inbound: bool,
229}
230
231pub struct SyncingEngine<B: BlockT, Client> {
232	/// Syncing strategy.
233	strategy: Box<dyn SyncingStrategy<B>>,
234
235	/// Blockchain client.
236	client: Arc<Client>,
237
238	/// Number of peers we're connected to.
239	num_connected: Arc<AtomicUsize>,
240
241	/// Are we actively catching up with the chain?
242	is_major_syncing: Arc<AtomicBool>,
243
244	/// Network service.
245	network_service: service::network::NetworkServiceHandle,
246
247	/// Channel for receiving service commands
248	service_rx: TracingUnboundedReceiver<ToServiceCommand<B>>,
249
250	/// Assigned roles.
251	roles: Roles,
252
253	/// Genesis hash.
254	genesis_hash: B::Hash,
255
256	/// Set of channels for other protocols that have subscribed to syncing events.
257	event_streams: Vec<TracingUnboundedSender<SyncEvent>>,
258
259	/// Interval at which we call `tick`.
260	tick_timeout: Interval,
261
262	/// All connected peers. Contains both full and light node peers.
263	peers: HashMap<PeerId, Peer<B>>,
264
265	/// List of nodes for which we perform additional logging because they are important for the
266	/// user.
267	important_peers: HashSet<PeerId>,
268
269	/// Actual list of connected no-slot nodes.
270	default_peers_set_no_slot_connected_peers: HashSet<PeerId>,
271
272	/// List of nodes that should never occupy peer slots.
273	default_peers_set_no_slot_peers: HashSet<PeerId>,
274
275	/// Value that was passed as part of the configuration. Used to cap the number of full
276	/// nodes.
277	default_peers_set_num_full: usize,
278
279	/// Number of slots to allocate to light nodes.
280	default_peers_set_num_light: usize,
281
282	/// Maximum number of inbound peers.
283	max_in_peers: usize,
284
285	/// Number of inbound peers accepted so far.
286	num_in_peers: usize,
287
288	/// Dynamic updatable no-slot peer set (see [`SyncingService::set_no_slot_peers`]).
289	/// Treated identically to `default_peers_set_no_slot_peers` for inbound slot accounting.
290	dynamic_no_slot_peers: HashSet<PeerId>,
291
292	/// Async processor of block announce validations.
293	block_announce_validator: BlockAnnounceValidatorStream<B>,
294
295	/// A cache for the data that was associated to a block announcement.
296	block_announce_data_cache: LruMap<B::Hash, Vec<u8>>,
297
298	/// The `PeerId`'s of all boot nodes.
299	boot_node_ids: HashSet<PeerId>,
300
301	/// Protocol name used for block announcements
302	block_announce_protocol_name: ProtocolName,
303
304	/// Prometheus metrics.
305	metrics: Option<Metrics>,
306
307	/// Handle that is used to communicate with `sc_network::Notifications`.
308	notification_service: Box<dyn NotificationService>,
309
310	/// Handle to `PeerStore`.
311	peer_store_handle: Arc<dyn PeerStoreProvider>,
312
313	/// Pending responses
314	pending_responses: PendingResponses,
315
316	/// Handle to import queue.
317	import_queue: Box<dyn ImportQueueService<B>>,
318}
319
320impl<B: BlockT, Client> SyncingEngine<B, Client>
321where
322	B: BlockT,
323	Client: HeaderBackend<B>
324		+ BlockBackend<B>
325		+ HeaderMetadata<B, Error = sp_blockchain::Error>
326		+ ProofProvider<B>
327		+ Send
328		+ Sync
329		+ 'static,
330{
331	pub fn new<N>(
332		roles: Roles,
333		client: Arc<Client>,
334		metrics_registry: Option<&Registry>,
335		network_metrics: NotificationMetrics,
336		net_config: &FullNetworkConfiguration<B, <B as BlockT>::Hash, N>,
337		protocol_id: ProtocolId,
338		fork_id: Option<&str>,
339		block_announce_validator: Box<dyn BlockAnnounceValidator<B> + Send>,
340		syncing_strategy: Box<dyn SyncingStrategy<B>>,
341		network_service: service::network::NetworkServiceHandle,
342		import_queue: Box<dyn ImportQueueService<B>>,
343		peer_store_handle: Arc<dyn PeerStoreProvider>,
344	) -> Result<(Self, SyncingService<B>, N::NotificationProtocolConfig), ClientError>
345	where
346		N: NetworkBackend<B, <B as BlockT>::Hash>,
347	{
348		let cache_capacity = (net_config.network_config.default_peers_set.in_peers +
349			net_config.network_config.default_peers_set.out_peers)
350			.max(1);
351		let important_peers = {
352			let mut imp_p = HashSet::new();
353			for reserved in &net_config.network_config.default_peers_set.reserved_nodes {
354				imp_p.insert(reserved.peer_id);
355			}
356			for config in net_config.notification_protocols() {
357				let peer_ids = config.set_config().reserved_nodes.iter().map(|info| info.peer_id);
358				imp_p.extend(peer_ids);
359			}
360
361			imp_p.shrink_to_fit();
362			imp_p
363		};
364		let boot_node_ids = {
365			let mut list = HashSet::new();
366			for node in &net_config.network_config.boot_nodes {
367				list.insert(node.peer_id);
368			}
369			list.shrink_to_fit();
370			list
371		};
372		let default_peers_set_no_slot_peers = {
373			let mut no_slot_p: HashSet<PeerId> = net_config
374				.network_config
375				.default_peers_set
376				.reserved_nodes
377				.iter()
378				.map(|reserved| reserved.peer_id)
379				.collect();
380			no_slot_p.shrink_to_fit();
381			no_slot_p
382		};
383		let default_peers_set_num_full =
384			net_config.network_config.default_peers_set_num_full as usize;
385		let default_peers_set_num_light = {
386			let total = net_config.network_config.default_peers_set.out_peers +
387				net_config.network_config.default_peers_set.in_peers;
388			total.saturating_sub(net_config.network_config.default_peers_set_num_full) as usize
389		};
390
391		let info = client.info();
392
393		let (block_announce_config, notification_service) =
394			Self::get_block_announce_proto_config::<N>(
395				protocol_id,
396				fork_id,
397				roles,
398				info.best_number,
399				info.best_hash,
400				info.genesis_hash,
401				&net_config.network_config.default_peers_set,
402				network_metrics,
403				Arc::clone(&peer_store_handle),
404			);
405
406		let block_announce_protocol_name = block_announce_config.protocol_name().clone();
407		let (tx, service_rx) = tracing_unbounded("mpsc_chain_sync", 100_000);
408		let num_connected = Arc::new(AtomicUsize::new(0));
409		let is_major_syncing = Arc::new(AtomicBool::new(false));
410
411		// `default_peers_set.in_peers` contains an unspecified amount of light peers so the number
412		// of full inbound peers must be calculated from the total full peer count
413		let max_full_peers = net_config.network_config.default_peers_set_num_full;
414		let max_out_peers = net_config.network_config.default_peers_set.out_peers;
415		let max_in_peers = (max_full_peers - max_out_peers) as usize;
416
417		let tick_timeout = {
418			let mut interval = tokio::time::interval(TICK_TIMEOUT);
419			interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
420			interval
421		};
422
423		Ok((
424			Self {
425				roles,
426				client,
427				strategy: syncing_strategy,
428				network_service,
429				peers: HashMap::new(),
430				block_announce_data_cache: LruMap::new(ByLength::new(cache_capacity)),
431				block_announce_protocol_name,
432				block_announce_validator: BlockAnnounceValidatorStream::new(
433					block_announce_validator,
434				),
435				num_connected: num_connected.clone(),
436				is_major_syncing: is_major_syncing.clone(),
437				service_rx,
438				genesis_hash: info.genesis_hash,
439				important_peers,
440				default_peers_set_no_slot_connected_peers: HashSet::new(),
441				boot_node_ids,
442				default_peers_set_no_slot_peers,
443				default_peers_set_num_full,
444				default_peers_set_num_light,
445				num_in_peers: 0usize,
446				max_in_peers,
447				dynamic_no_slot_peers: HashSet::new(),
448				event_streams: Vec::new(),
449				notification_service,
450				tick_timeout,
451				peer_store_handle,
452				metrics: if let Some(r) = metrics_registry {
453					match Metrics::register(r, is_major_syncing.clone(), num_connected.clone()) {
454						Ok(metrics) => Some(metrics),
455						Err(err) => {
456							log::error!(target: LOG_TARGET, "Failed to register metrics {err:?}");
457							None
458						},
459					}
460				} else {
461					None
462				},
463				pending_responses: PendingResponses::new(),
464				import_queue,
465			},
466			SyncingService::new(tx, num_connected, is_major_syncing),
467			block_announce_config,
468		))
469	}
470
471	fn update_peer_info(
472		&mut self,
473		peer_id: &PeerId,
474		best_hash: B::Hash,
475		best_number: NumberFor<B>,
476	) {
477		if let Some(ref mut peer) = self.peers.get_mut(peer_id) {
478			peer.info.best_hash = best_hash;
479			peer.info.best_number = best_number;
480		}
481	}
482
483	/// Process the result of the block announce validation.
484	fn process_block_announce_validation_result(
485		&mut self,
486		validation_result: BlockAnnounceValidationResult<B::Header>,
487	) {
488		match validation_result {
489			BlockAnnounceValidationResult::Skip { peer_id: _ } => {},
490			BlockAnnounceValidationResult::Process { is_new_best, peer_id, announce } => {
491				if let Some((best_hash, best_number)) =
492					self.strategy.on_validated_block_announce(is_new_best, peer_id, &announce)
493				{
494					self.update_peer_info(&peer_id, best_hash, best_number);
495				}
496
497				if let Some(data) = announce.data {
498					if !data.is_empty() {
499						self.block_announce_data_cache.insert(announce.header.hash(), data);
500					}
501				}
502			},
503			BlockAnnounceValidationResult::Failure { peer_id, disconnect } => {
504				if disconnect {
505					log::debug!(
506						target: LOG_TARGET,
507						"Disconnecting peer {peer_id} due to block announce validation failure",
508					);
509					self.network_service
510						.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
511				}
512
513				self.network_service.report_peer(peer_id, rep::BAD_BLOCK_ANNOUNCEMENT);
514			},
515		}
516	}
517
518	/// Push a block announce validation.
519	pub fn push_block_announce_validation(
520		&mut self,
521		peer_id: PeerId,
522		announce: BlockAnnounce<B::Header>,
523	) {
524		let hash = announce.header.hash();
525
526		let peer = match self.peers.get_mut(&peer_id) {
527			Some(p) => p,
528			None => {
529				log::error!(
530					target: LOG_TARGET,
531					"Received block announce from disconnected peer {peer_id}",
532				);
533				debug_assert!(false);
534				return;
535			},
536		};
537		peer.known_blocks.insert(hash);
538
539		if peer.info.roles.is_full() {
540			let is_best = match announce.state.unwrap_or(BlockState::Best) {
541				BlockState::Best => true,
542				BlockState::Normal => false,
543			};
544
545			self.block_announce_validator
546				.push_block_announce_validation(peer_id, hash, announce, is_best);
547		}
548	}
549
550	/// Make sure an important block is propagated to peers.
551	///
552	/// In chain-based consensus, we often need to make sure non-best forks are
553	/// at least temporarily synced.
554	pub fn announce_block(&mut self, hash: B::Hash, data: Option<Vec<u8>>) {
555		let header = match self.client.header(hash) {
556			Ok(Some(header)) => header,
557			Ok(None) => {
558				log::warn!(target: LOG_TARGET, "Trying to announce unknown block: {hash}");
559				return;
560			},
561			Err(e) => {
562				log::warn!(target: LOG_TARGET, "Error reading block header {hash}: {e}");
563				return;
564			},
565		};
566
567		// don't announce genesis block since it will be ignored
568		if header.number().is_zero() {
569			return;
570		}
571
572		let is_best = self.client.info().best_hash == hash;
573		log::debug!(target: LOG_TARGET, "Reannouncing block {hash:?} is_best: {is_best}");
574
575		let data = data
576			.or_else(|| self.block_announce_data_cache.get(&hash).cloned())
577			.unwrap_or_default();
578
579		for (peer_id, ref mut peer) in self.peers.iter_mut() {
580			let inserted = peer.known_blocks.insert(hash);
581			if inserted {
582				log::trace!(target: LOG_TARGET, "Announcing block {hash:?} to {peer_id}");
583				let message = BlockAnnounce {
584					header: header.clone(),
585					state: if is_best { Some(BlockState::Best) } else { Some(BlockState::Normal) },
586					data: Some(data.clone()),
587				};
588
589				let _ = self.notification_service.send_sync_notification(peer_id, message.encode());
590			}
591		}
592	}
593
594	pub async fn run(mut self) {
595		loop {
596			tokio::select! {
597				_ = self.tick_timeout.tick() => {
598					// TODO: This tick should not be necessary, but
599					//  `self.process_strategy_actions()` is not called in some cases otherwise and
600					//  some tests fail because of this
601				},
602				command = self.service_rx.select_next_some() =>
603					self.process_service_command(command),
604				notification_event = self.notification_service.next_event() => match notification_event {
605					Some(event) => self.process_notification_event(event),
606					None => {
607						error!(
608							target: LOG_TARGET,
609							"Terminating `SyncingEngine` because `NotificationService` has terminated.",
610						);
611
612						return;
613					}
614				},
615				response_event = self.pending_responses.select_next_some() =>
616					self.process_response_event(response_event),
617				validation_result = self.block_announce_validator.select_next_some() =>
618					self.process_block_announce_validation_result(validation_result),
619			}
620
621			// Update atomic variables
622			self.is_major_syncing.store(self.strategy.is_major_syncing(), Ordering::Relaxed);
623
624			// Process actions requested by a syncing strategy.
625			if let Err(e) = self.process_strategy_actions() {
626				error!(
627					target: LOG_TARGET,
628					"Terminating `SyncingEngine` due to fatal error: {e:?}.",
629				);
630				return;
631			}
632		}
633	}
634
635	fn process_strategy_actions(&mut self) -> Result<(), ClientError> {
636		for action in self.strategy.actions(&self.network_service)? {
637			match action {
638				SyncingAction::StartRequest { peer_id, key, request } => {
639					if !self.peers.contains_key(&peer_id) {
640						trace!(
641							target: LOG_TARGET,
642							"Cannot start request with strategy key {key:?} to unknown peer \
643							{peer_id}",
644						);
645						debug_assert!(false);
646						continue;
647					}
648
649					self.pending_responses.insert(peer_id, key, request);
650				},
651				SyncingAction::CancelRequest { peer_id, key } => {
652					let removed = self.pending_responses.remove(peer_id, key);
653
654					trace!(
655						target: LOG_TARGET,
656						"Processed `SyncingAction::CancelRequest`, response removed: {removed}.",
657					);
658				},
659				SyncingAction::DropPeer(BadPeer(peer_id, rep)) => {
660					self.pending_responses.remove_all(&peer_id);
661					self.network_service
662						.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
663					self.network_service.report_peer(peer_id, rep);
664
665					trace!(target: LOG_TARGET, "{peer_id:?} dropped: {rep:?}.");
666				},
667				SyncingAction::ImportBlocks { origin, blocks } => {
668					let count = blocks.len();
669					self.import_blocks(origin, blocks);
670
671					trace!(
672						target: LOG_TARGET,
673						"Processed `ChainSyncAction::ImportBlocks` with {count} blocks.",
674					);
675				},
676				SyncingAction::ImportJustifications { peer_id, hash, number, justifications } => {
677					self.import_justifications(peer_id, hash, number, justifications);
678
679					trace!(
680						target: LOG_TARGET,
681						"Processed `ChainSyncAction::ImportJustifications` from peer {} for block {} ({}).",
682						peer_id,
683						hash,
684						number,
685					)
686				},
687				// Nothing to do, this is handled internally by `PolkadotSyncingStrategy`.
688				SyncingAction::Finished => {},
689			}
690		}
691
692		Ok(())
693	}
694
695	/// Reconcile per-peer slot tracking against `new_dynamic_no_slot`. See
696	/// [`apply_no_slot_set_inner`] for details.
697	fn apply_no_slot_set(&mut self, new_dynamic_no_slot: HashSet<PeerId>) {
698		let connected_peers = &self.peers;
699		apply_no_slot_set_inner(
700			|peer_id| {
701				connected_peers
702					.get(peer_id)
703					.map(|peer| peer.inbound && peer.info.roles.is_full())
704			},
705			&self.default_peers_set_no_slot_peers,
706			&self.dynamic_no_slot_peers,
707			&new_dynamic_no_slot,
708			&mut self.default_peers_set_no_slot_connected_peers,
709			&mut self.num_in_peers,
710			self.max_in_peers,
711			&self.network_service,
712			&self.block_announce_protocol_name,
713		);
714		self.dynamic_no_slot_peers = new_dynamic_no_slot;
715	}
716
717	fn process_service_command(&mut self, command: ToServiceCommand<B>) {
718		match command {
719			ToServiceCommand::SetSyncForkRequest(peers, hash, number) => {
720				self.strategy.set_sync_fork_request(peers, &hash, number);
721			},
722			ToServiceCommand::EventStream(tx) => {
723				// Let a new subscriber know about already connected peers.
724				for (peer_id, peer) in self.peers.iter() {
725					let _ = tx.unbounded_send(SyncEvent::PeerConnected {
726						peer_id: *peer_id,
727						roles: peer.info.roles,
728					});
729				}
730				self.event_streams.push(tx);
731			},
732			ToServiceCommand::RequestJustification(hash, number) => {
733				self.strategy.request_justification(&hash, number)
734			},
735			ToServiceCommand::ClearJustificationRequests => {
736				self.strategy.clear_justification_requests()
737			},
738			ToServiceCommand::BlocksProcessed(imported, count, results) => {
739				self.strategy.on_blocks_processed(imported, count, results);
740			},
741			ToServiceCommand::JustificationImported(peer_id, hash, number, import_result) => {
742				let success =
743					matches!(import_result, sc_consensus::JustificationImportResult::Success);
744				self.strategy.on_justification_import(hash, number, success);
745
746				match import_result {
747					sc_consensus::JustificationImportResult::OutdatedJustification => {
748						log::info!(
749							target: LOG_TARGET,
750							"๐Ÿ’” Outdated justification provided by {peer_id} for #{hash}",
751						);
752					},
753					sc_consensus::JustificationImportResult::Failure => {
754						log::info!(
755							target: LOG_TARGET,
756							"๐Ÿ’” Invalid justification provided by {peer_id} for #{hash}",
757						);
758						self.network_service
759							.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
760						self.network_service.report_peer(
761							peer_id,
762							ReputationChange::new_fatal("Invalid justification"),
763						);
764					},
765					sc_consensus::JustificationImportResult::Success => {
766						log::debug!(
767							target: LOG_TARGET,
768							"Justification for block #{hash} ({number}) imported from {peer_id} successfully",
769						);
770					},
771				}
772			},
773			ToServiceCommand::AnnounceBlock(hash, data) => self.announce_block(hash, data),
774			ToServiceCommand::NewBestBlockImported(hash, number) => {
775				log::debug!(target: LOG_TARGET, "New best block imported {:?}/#{}", hash, number);
776
777				self.strategy.update_chain_info(&hash, number);
778				let _ = self.notification_service.try_set_handshake(
779					BlockAnnouncesHandshake::<B>::build(
780						self.roles,
781						number,
782						hash,
783						self.genesis_hash,
784					)
785					.encode(),
786				);
787			},
788			ToServiceCommand::Status(tx) => {
789				let _ = tx.send(self.strategy.status());
790			},
791			ToServiceCommand::NumActivePeers(tx) => {
792				let _ = tx.send(self.num_active_peers());
793			},
794			ToServiceCommand::NumDownloadedBlocks(tx) => {
795				let _ = tx.send(self.strategy.num_downloaded_blocks());
796			},
797			ToServiceCommand::NumSyncRequests(tx) => {
798				let _ = tx.send(self.strategy.num_sync_requests());
799			},
800			ToServiceCommand::PeersInfo(tx) => {
801				let peers_info =
802					self.peers.iter().map(|(peer_id, peer)| (*peer_id, peer.info)).collect();
803				let _ = tx.send(peers_info);
804			},
805			ToServiceCommand::SetNoSlotPeers(peers) => self.apply_no_slot_set(peers),
806			ToServiceCommand::OnBlockFinalized(hash, header) => {
807				self.strategy.on_block_finalized(&hash, *header.number())
808			},
809		}
810	}
811
812	fn process_notification_event(&mut self, event: NotificationEvent) {
813		match event {
814			NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx } => {
815				let validation_result = self
816					.validate_connection(&peer, handshake, Direction::Inbound)
817					.map_or(ValidationResult::Reject, |_| ValidationResult::Accept);
818
819				let _ = result_tx.send(validation_result);
820			},
821			NotificationEvent::NotificationStreamOpened { peer, handshake, direction, .. } => {
822				log::debug!(
823					target: LOG_TARGET,
824					"Substream opened for {peer}, handshake {handshake:?}"
825				);
826
827				match self.validate_connection(&peer, handshake, direction) {
828					Ok(handshake) => {
829						if self.on_sync_peer_connected(peer, &handshake, direction).is_err() {
830							log::debug!(target: LOG_TARGET, "Failed to register peer {peer}");
831							self.network_service
832								.disconnect_peer(peer, self.block_announce_protocol_name.clone());
833						}
834					},
835					Err(wrong_genesis) => {
836						log::debug!(target: LOG_TARGET, "`SyncingEngine` rejected {peer}");
837
838						if wrong_genesis {
839							self.peer_store_handle.report_peer(peer, rep::GENESIS_MISMATCH);
840						}
841
842						self.network_service
843							.disconnect_peer(peer, self.block_announce_protocol_name.clone());
844					},
845				}
846			},
847			NotificationEvent::NotificationStreamClosed { peer } => {
848				self.on_sync_peer_disconnected(peer);
849			},
850			NotificationEvent::NotificationReceived { peer, notification } => {
851				if !self.peers.contains_key(&peer) {
852					log::error!(
853						target: LOG_TARGET,
854						"received notification from {peer} who had been earlier refused by `SyncingEngine`",
855					);
856					return;
857				}
858
859				let Ok(announce) = BlockAnnounce::decode(&mut notification.as_ref()) else {
860					log::warn!(target: LOG_TARGET, "failed to decode block announce");
861					return;
862				};
863
864				self.push_block_announce_validation(peer, announce);
865			},
866		}
867	}
868
869	fn is_no_slot_peer(&self, peer_id: &PeerId) -> bool {
870		self.default_peers_set_no_slot_peers.contains(peer_id) ||
871			self.dynamic_no_slot_peers.contains(peer_id)
872	}
873
874	/// Called by peer when it is disconnecting.
875	///
876	/// Returns a result if the handshake of this peer was indeed accepted.
877	fn on_sync_peer_disconnected(&mut self, peer_id: PeerId) {
878		let Some(info) = self.peers.remove(&peer_id) else {
879			log::debug!(target: LOG_TARGET, "{peer_id} does not exist in `SyncingEngine`");
880			return;
881		};
882		if let Some(metrics) = &self.metrics {
883			metrics.peers.dec();
884		}
885		self.num_connected.fetch_sub(1, Ordering::AcqRel);
886
887		if self.important_peers.contains(&peer_id) {
888			log::warn!(target: LOG_TARGET, "Reserved peer {peer_id} disconnected");
889		} else {
890			log::debug!(target: LOG_TARGET, "{peer_id} disconnected");
891		}
892
893		if !self.default_peers_set_no_slot_connected_peers.remove(&peer_id) &&
894			info.inbound &&
895			info.info.roles.is_full()
896		{
897			match self.num_in_peers.checked_sub(1) {
898				Some(value) => {
899					self.num_in_peers = value;
900				},
901				None => {
902					log::error!(
903						target: LOG_TARGET,
904						"trying to disconnect an inbound node which is not counted as inbound"
905					);
906					debug_assert!(false);
907				},
908			}
909		}
910
911		self.strategy.remove_peer(&peer_id);
912		self.pending_responses.remove_all(&peer_id);
913		self.event_streams
914			.retain(|stream| stream.unbounded_send(SyncEvent::PeerDisconnected(peer_id)).is_ok());
915	}
916
917	/// Validate received handshake.
918	fn validate_handshake(
919		&mut self,
920		peer_id: &PeerId,
921		handshake: Vec<u8>,
922	) -> Result<BlockAnnouncesHandshake<B>, bool> {
923		log::trace!(target: LOG_TARGET, "Validate handshake for {peer_id}");
924
925		let handshake = <BlockAnnouncesHandshake<B> as DecodeAll>::decode_all(&mut &handshake[..])
926			.map_err(|error| {
927				log::debug!(target: LOG_TARGET, "Failed to decode handshake for {peer_id}: {error:?}");
928				false
929			})?;
930
931		if handshake.genesis_hash != self.genesis_hash {
932			if self.important_peers.contains(&peer_id) {
933				log::error!(
934					target: LOG_TARGET,
935					"Reserved peer id `{peer_id}` is on a different chain (our genesis: {} theirs: {})",
936					self.genesis_hash,
937					handshake.genesis_hash,
938				);
939			} else if self.boot_node_ids.contains(&peer_id) {
940				log::error!(
941					target: LOG_TARGET,
942					"Bootnode with peer id `{peer_id}` is on a different chain (our genesis: {} theirs: {})",
943					self.genesis_hash,
944					handshake.genesis_hash,
945				);
946			} else {
947				log::debug!(
948					target: LOG_TARGET,
949					"Peer is on different chain (our genesis: {} theirs: {})",
950					self.genesis_hash,
951					handshake.genesis_hash
952				);
953			}
954
955			return Err(true);
956		}
957
958		Ok(handshake)
959	}
960
961	/// Validate connection.
962	// NOTE Returning `Err(bool)` is a really ugly hack to work around the issue
963	// that `ProtocolController` thinks the peer is connected when in fact it can
964	// still be under validation. If the peer has different genesis than the
965	// local node the validation fails but the peer cannot be reported in
966	// `validate_connection()` as that is also called by
967	// `ValidateInboundSubstream` which means that the peer is still being
968	// validated and banning the peer when handling that event would
969	// result in peer getting dropped twice.
970	//
971	// The proper way to fix this is to integrate `ProtocolController` more
972	// tightly with `NotificationService` or add an additional API call for
973	// banning pre-accepted peers (which is not desirable)
974	fn validate_connection(
975		&mut self,
976		peer_id: &PeerId,
977		handshake: Vec<u8>,
978		direction: Direction,
979	) -> Result<BlockAnnouncesHandshake<B>, bool> {
980		log::trace!(target: LOG_TARGET, "New peer {peer_id} {handshake:?}");
981
982		let handshake = self.validate_handshake(peer_id, handshake)?;
983
984		if self.peers.contains_key(&peer_id) {
985			log::error!(
986				target: LOG_TARGET,
987				"Called `validate_connection()` with already connected peer {peer_id}",
988			);
989			debug_assert!(false);
990			return Err(false);
991		}
992
993		let no_slot_peer = self.is_no_slot_peer(&peer_id);
994		let this_peer_reserved_slot: usize = if no_slot_peer { 1 } else { 0 };
995
996		if handshake.roles.is_full() &&
997			self.strategy.num_peers() >=
998				self.default_peers_set_num_full +
999					self.default_peers_set_no_slot_connected_peers.len() +
1000					this_peer_reserved_slot
1001		{
1002			log::debug!(
1003				target: LOG_TARGET,
1004				"Too many full nodes, rejecting {peer_id} (no_slot_peer={no_slot_peer}, num_peers={}, full_cap={}, no_slot_connected={}, this_reserved={})",
1005				self.strategy.num_peers(),
1006				self.default_peers_set_num_full,
1007				self.default_peers_set_no_slot_connected_peers.len(),
1008				this_peer_reserved_slot,
1009			);
1010			return Err(false);
1011		}
1012
1013		// make sure to accept no more than `--in-peers` many full nodes
1014		if !no_slot_peer &&
1015			handshake.roles.is_full() &&
1016			direction.is_inbound() &&
1017			self.num_in_peers >= self.max_in_peers
1018		{
1019			if self.num_in_peers > self.max_in_peers {
1020				log::warn!(
1021					target: LOG_TARGET,
1022					"num_in_peers ({}) exceeds max_in_peers ({}), this is a slot accounting bug ",
1023					self.num_in_peers,
1024					self.max_in_peers,
1025				);
1026				debug_assert!(false);
1027			}
1028			log::debug!(
1029				target: LOG_TARGET,
1030				"All inbound slots have been consumed, rejecting {peer_id} (no_slot_peer={no_slot_peer}, num_in_peers={}, max_in_peers={})",
1031				self.num_in_peers,
1032				self.max_in_peers,
1033			);
1034			return Err(false);
1035		}
1036
1037		// make sure that all slots are not occupied by light peers
1038		//
1039		// `ChainSync` only accepts full peers whereas `SyncingEngine` accepts both full and light
1040		// peers. Verify that there is a slot in `SyncingEngine` for the inbound light peer
1041		if handshake.roles.is_light() &&
1042			(self.peers.len() - self.strategy.num_peers()) >= self.default_peers_set_num_light
1043		{
1044			log::debug!(target: LOG_TARGET, "Too many light nodes, rejecting {peer_id}");
1045			return Err(false);
1046		}
1047
1048		Ok(handshake)
1049	}
1050
1051	/// Called on the first connection between two peers on the default set, after their exchange
1052	/// of handshake.
1053	///
1054	/// Returns `Ok` if the handshake is accepted and the peer added to the list of peers we sync
1055	/// from.
1056	fn on_sync_peer_connected(
1057		&mut self,
1058		peer_id: PeerId,
1059		status: &BlockAnnouncesHandshake<B>,
1060		direction: Direction,
1061	) -> Result<(), ()> {
1062		log::trace!(target: LOG_TARGET, "New peer {peer_id} {status:?}");
1063
1064		let peer = Peer {
1065			info: ExtendedPeerInfo {
1066				roles: status.roles,
1067				best_hash: status.best_hash,
1068				best_number: status.best_number,
1069			},
1070			known_blocks: LruHashSet::new(
1071				NonZeroUsize::new(MAX_KNOWN_BLOCKS).expect("Constant is nonzero"),
1072			),
1073			inbound: direction.is_inbound(),
1074		};
1075
1076		// Only forward full peers to syncing strategy.
1077		if status.roles.is_full() {
1078			self.strategy.add_peer(peer_id, peer.info.best_hash, peer.info.best_number);
1079		}
1080
1081		log::debug!(target: LOG_TARGET, "Connected {peer_id}");
1082
1083		if self.peers.insert(peer_id, peer).is_none() {
1084			if let Some(metrics) = &self.metrics {
1085				metrics.peers.inc();
1086			}
1087			self.num_connected.fetch_add(1, Ordering::AcqRel);
1088		}
1089		self.peer_store_handle.set_peer_role(&peer_id, status.roles.into());
1090
1091		if self.is_no_slot_peer(&peer_id) {
1092			self.default_peers_set_no_slot_connected_peers.insert(peer_id);
1093		} else if direction.is_inbound() && status.roles.is_full() {
1094			self.num_in_peers += 1;
1095		}
1096
1097		self.event_streams.retain(|stream| {
1098			stream
1099				.unbounded_send(SyncEvent::PeerConnected { peer_id, roles: status.roles })
1100				.is_ok()
1101		});
1102
1103		Ok(())
1104	}
1105
1106	fn process_response_event(&mut self, response_event: ResponseEvent) {
1107		let ResponseEvent { peer_id, key, response: response_result } = response_event;
1108
1109		match response_result {
1110			Ok(Ok((response, protocol_name))) => {
1111				self.strategy.on_generic_response(&peer_id, key, protocol_name, response);
1112			},
1113			Ok(Err(e)) => {
1114				debug!(target: LOG_TARGET, "Request to peer {peer_id:?} failed: {e:?}.");
1115
1116				match e {
1117					RequestFailure::Network(OutboundFailure::Timeout) => {
1118						self.network_service.report_peer(peer_id, rep::TIMEOUT);
1119						self.network_service
1120							.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
1121					},
1122					RequestFailure::Network(OutboundFailure::UnsupportedProtocols) => {
1123						self.network_service.report_peer(peer_id, rep::BAD_PROTOCOL);
1124						self.network_service
1125							.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
1126					},
1127					RequestFailure::Network(OutboundFailure::DialFailure) => {
1128						self.network_service
1129							.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
1130					},
1131					RequestFailure::Refused => {
1132						self.network_service.report_peer(peer_id, rep::REFUSED);
1133						self.network_service
1134							.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
1135					},
1136					RequestFailure::Network(OutboundFailure::ConnectionClosed) |
1137					RequestFailure::NotConnected => {
1138						self.network_service
1139							.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
1140					},
1141					RequestFailure::UnknownProtocol => {
1142						debug_assert!(false, "Block request protocol should always be known.");
1143					},
1144					RequestFailure::InvalidRequest => {
1145						debug_assert!(false, "Block request payload should always be valid.");
1146					},
1147					RequestFailure::Obsolete => {
1148						debug_assert!(
1149							false,
1150							"Can not receive `RequestFailure::Obsolete` after dropping the \
1151							response receiver.",
1152						);
1153					},
1154					RequestFailure::Network(OutboundFailure::Io(_)) => {
1155						self.network_service.report_peer(peer_id, rep::IO);
1156						self.network_service
1157							.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
1158					},
1159				}
1160			},
1161			Err(oneshot::Canceled) => {
1162				trace!(
1163					target: LOG_TARGET,
1164					"Request to peer {peer_id:?} failed due to oneshot being canceled.",
1165				);
1166				self.network_service
1167					.disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
1168			},
1169		}
1170	}
1171
1172	/// Returns the number of peers we're connected to and that are being queried.
1173	fn num_active_peers(&self) -> usize {
1174		self.pending_responses.len()
1175	}
1176
1177	/// Get config for the block announcement protocol
1178	fn get_block_announce_proto_config<N: NetworkBackend<B, <B as BlockT>::Hash>>(
1179		protocol_id: ProtocolId,
1180		fork_id: Option<&str>,
1181		roles: Roles,
1182		best_number: NumberFor<B>,
1183		best_hash: B::Hash,
1184		genesis_hash: B::Hash,
1185		set_config: &SetConfig,
1186		metrics: NotificationMetrics,
1187		peer_store_handle: Arc<dyn PeerStoreProvider>,
1188	) -> (N::NotificationProtocolConfig, Box<dyn NotificationService>) {
1189		let block_announces_protocol = block_announces_protocol_name(genesis_hash, fork_id);
1190
1191		N::notification_config(
1192			block_announces_protocol.into(),
1193			iter::once(block_announces_legacy_protocol_name(&protocol_id).into()).collect(),
1194			MAX_BLOCK_ANNOUNCE_SIZE,
1195			Some(NotificationHandshake::new(BlockAnnouncesHandshake::<B>::build(
1196				roles,
1197				best_number,
1198				best_hash,
1199				genesis_hash,
1200			))),
1201			set_config.clone(),
1202			metrics,
1203			peer_store_handle,
1204		)
1205	}
1206
1207	/// Import blocks.
1208	fn import_blocks(&mut self, origin: BlockOrigin, blocks: Vec<IncomingBlock<B>>) {
1209		if let Some(metrics) = &self.metrics {
1210			metrics.import_queue_blocks_submitted.inc();
1211		}
1212
1213		self.import_queue.import_blocks(origin, blocks);
1214	}
1215
1216	/// Import justifications.
1217	fn import_justifications(
1218		&mut self,
1219		peer_id: PeerId,
1220		hash: B::Hash,
1221		number: NumberFor<B>,
1222		justifications: Justifications,
1223	) {
1224		if let Some(metrics) = &self.metrics {
1225			metrics.import_queue_justifications_submitted.inc();
1226		}
1227
1228		self.import_queue.import_justifications(peer_id, hash, number, justifications);
1229	}
1230}
1231
1232/// Update per-peer slot tracking for changes in the dynamic no-slot set.
1233/// Promotes newly added peers, demotes removed ones, ignoring static no-slot peers.
1234///
1235/// `peer_inbound_full(peer_id)` returns `true` if `peer_id` is inbound and full.
1236///  Returns `None` if the peer is not connected.
1237///
1238/// If removing a peer from no-slot would make `num_in_peers` exceed `max_in_peers`,
1239/// disconnect the peer instead and keep it in `connected_no_slot` until the async disconnect
1240/// handler will update `num_in_peers`..
1241/// Caller needs to update `dynamic_no_slot_peers` after calling this function.
1242fn apply_no_slot_set_inner(
1243	peer_inbound_full: impl Fn(&PeerId) -> Option<bool>,
1244	static_no_slot: &HashSet<PeerId>,
1245	old_dynamic_no_slot: &HashSet<PeerId>,
1246	new_dynamic_no_slot: &HashSet<PeerId>,
1247	connected_no_slot: &mut HashSet<PeerId>,
1248	num_in_peers: &mut usize,
1249	max_in_peers: usize,
1250	network_service: &service::network::NetworkServiceHandle,
1251	protocol: &ProtocolName,
1252) {
1253	// Skip static-set and disconnected peers and return the slot-affecting flag for the rest.
1254	let slot_impact = |peer_id: &PeerId| -> Option<bool> {
1255		if static_no_slot.contains(peer_id) {
1256			return None;
1257		}
1258		peer_inbound_full(peer_id)
1259	};
1260
1261	let mut promoted = 0;
1262	let mut demoted = 0;
1263	let mut disconnected = 0;
1264
1265	for peer_id in new_dynamic_no_slot.difference(old_dynamic_no_slot) {
1266		let Some(affects_slots) = slot_impact(peer_id) else { continue };
1267		// Defensive check, should never happen as we filter above.
1268		if !connected_no_slot.insert(*peer_id) {
1269			log::error!(
1270				target: LOG_TARGET,
1271				"{peer_id} promoted to no-slot but was already in connected_no_slot",
1272			);
1273			debug_assert!(false);
1274			continue;
1275		}
1276		if affects_slots {
1277			if let Some(n) = num_in_peers.checked_sub(1) {
1278				*num_in_peers = n;
1279			} else {
1280				log::error!(
1281					target: LOG_TARGET,
1282					"num_in_peers underflow promoting {peer_id} to no-slot",
1283				);
1284				debug_assert!(false);
1285			}
1286			promoted += 1;
1287		}
1288	}
1289
1290	for peer_id in old_dynamic_no_slot.difference(new_dynamic_no_slot) {
1291		let Some(affects_slots) = slot_impact(peer_id) else { continue };
1292		if !connected_no_slot.contains(peer_id) {
1293			continue;
1294		}
1295		if affects_slots && *num_in_peers >= max_in_peers {
1296			log::debug!(
1297				target: LOG_TARGET,
1298				"Demoting {peer_id} would exceed max_in_peers ({max_in_peers}); disconnecting",
1299			);
1300			network_service.disconnect_peer(*peer_id, protocol.clone());
1301			disconnected += 1;
1302			continue;
1303		}
1304		connected_no_slot.remove(peer_id);
1305		if affects_slots {
1306			*num_in_peers += 1;
1307			demoted += 1;
1308		}
1309	}
1310
1311	log::debug!(
1312		target: LOG_TARGET,
1313		"Dynamic no-slot peer set updated: {} peers: +{} in, -{} out, {} disconnected",
1314		new_dynamic_no_slot.len(),
1315		promoted,
1316		demoted,
1317		disconnected,
1318	);
1319}
1320
1321#[cfg(test)]
1322mod tests {
1323	use super::*;
1324
1325	fn fresh_peers<const N: usize>() -> [PeerId; N] {
1326		std::array::from_fn(|_| PeerId::random())
1327	}
1328
1329	fn set_of<const N: usize>(peers: [PeerId; N]) -> HashSet<PeerId> {
1330		peers.into_iter().collect()
1331	}
1332
1333	/// Run [`apply_no_slot_set`] with the given initial state. Uses `usize::MAX` for
1334	/// `max_in_peers` so demotion never trips the disconnect path. Returns the final
1335	/// `connected_no_slot` set and `num_in_peers`.
1336	#[track_caller]
1337	fn run_apply(
1338		connected: Vec<(PeerId, bool)>,
1339		static_no_slot: HashSet<PeerId>,
1340		old_dynamic: HashSet<PeerId>,
1341		new_dynamic: HashSet<PeerId>,
1342		initial_connected_no_slot: HashSet<PeerId>,
1343		initial_num_in_peers: usize,
1344	) -> (HashSet<PeerId>, usize) {
1345		let (connected_no_slot, num_in, disconnects) = run_apply_with_cap(
1346			connected,
1347			static_no_slot,
1348			old_dynamic,
1349			new_dynamic,
1350			initial_connected_no_slot,
1351			initial_num_in_peers,
1352			usize::MAX,
1353		);
1354		assert!(disconnects.is_empty(), "unexpected disconnects: {disconnects:?}");
1355		(connected_no_slot, num_in)
1356	}
1357
1358	/// Variant of [`run_apply`] that exposes `max_in_peers` and the list of peers the
1359	/// function asked the network to disconnect (drained from the `NetworkServiceHandle`'s
1360	/// command channel).
1361	#[track_caller]
1362	fn run_apply_with_cap(
1363		connected: Vec<(PeerId, bool)>,
1364		static_no_slot: HashSet<PeerId>,
1365		old_dynamic: HashSet<PeerId>,
1366		new_dynamic: HashSet<PeerId>,
1367		initial_connected_no_slot: HashSet<PeerId>,
1368		initial_num_in_peers: usize,
1369		max_in_peers: usize,
1370	) -> (HashSet<PeerId>, usize, Vec<PeerId>) {
1371		use crate::service::network::{NetworkServiceHandle, ToServiceCommand as NetCmd};
1372
1373		let peer_inbound_full: HashMap<PeerId, bool> = connected.into_iter().collect();
1374		let (tx, mut rx) = tracing_unbounded::<NetCmd>("test_apply_no_slot_set_disconnects", 100);
1375		let network_service = NetworkServiceHandle::new(tx);
1376		let protocol: ProtocolName = "/test/block-announces/1".into();
1377		let mut connected_no_slot = initial_connected_no_slot;
1378		let mut num_in_peers = initial_num_in_peers;
1379		apply_no_slot_set_inner(
1380			|peer_id| peer_inbound_full.get(peer_id).copied(),
1381			&static_no_slot,
1382			&old_dynamic,
1383			&new_dynamic,
1384			&mut connected_no_slot,
1385			&mut num_in_peers,
1386			max_in_peers,
1387			&network_service,
1388			&protocol,
1389		);
1390		drop(network_service);
1391
1392		let mut disconnects = Vec::new();
1393		while let Ok(cmd) = rx.try_recv() {
1394			if let NetCmd::DisconnectPeer(peer, _) = cmd {
1395				disconnects.push(peer);
1396			}
1397		}
1398		(connected_no_slot, num_in_peers, disconnects)
1399	}
1400
1401	#[test]
1402	fn apply_promotes_multiple_inbound_full_peers() {
1403		// `already` is in both old and new dynamic โ€” it must stay in `connected_no_slot`
1404		// without releasing another slot.
1405		let [a, b, c, already] = fresh_peers();
1406		let (connected_no_slot, num_in) = run_apply(
1407			vec![(a, true), (b, true), (c, true), (already, true)],
1408			HashSet::new(),
1409			set_of([already]),
1410			set_of([a, b, c, already]),
1411			set_of([already]),
1412			10,
1413		);
1414		assert_eq!(connected_no_slot, set_of([a, b, c, already]));
1415		assert_eq!(num_in, 7);
1416	}
1417
1418	#[test]
1419	fn apply_demotes_multiple_inbound_full_peers() {
1420		let [a, b, c, stays] = fresh_peers();
1421		let (connected_no_slot, num_in) = run_apply(
1422			vec![(a, true), (b, true), (c, true), (stays, true)],
1423			HashSet::new(),
1424			set_of([a, b, c, stays]),
1425			set_of([stays]),
1426			set_of([a, b, c, stays]),
1427			2,
1428		);
1429		assert_eq!(connected_no_slot, set_of([stays]));
1430		assert_eq!(num_in, 5);
1431	}
1432
1433	#[test]
1434	fn apply_ignores_non_slot_consuming_peers() {
1435		// Outbound peers and inbound light peers both yield `affects_slots = false`. Either
1436		// kind transitioning must update `connected_no_slot` but not move `num_in_peers`.
1437		let [outbound, light] = fresh_peers();
1438		let (connected_no_slot, num_in) = run_apply(
1439			vec![(outbound, false), (light, false)],
1440			HashSet::new(),
1441			HashSet::new(),
1442			set_of([outbound, light]),
1443			HashSet::new(),
1444			5,
1445		);
1446		assert_eq!(connected_no_slot, set_of([outbound, light]));
1447		assert_eq!(num_in, 5);
1448	}
1449
1450	#[test]
1451	fn apply_static_peers_stay_no_slot_when_removed_from_dynamic() {
1452		// `control` (dynamic-only) IS demoted, proving the wiring is live โ€” the static peers
1453		// must NOT be demoted because the static set takes precedence over the dynamic one.
1454		let [s1, s2, control] = fresh_peers();
1455		let (connected_no_slot, num_in) = run_apply(
1456			vec![(s1, true), (s2, true), (control, true)],
1457			set_of([s1, s2]),
1458			set_of([s1, s2, control]),
1459			HashSet::new(),
1460			set_of([s1, s2, control]),
1461			2,
1462		);
1463		assert_eq!(connected_no_slot, set_of([s1, s2]));
1464		assert_eq!(num_in, 3);
1465	}
1466
1467	#[test]
1468	fn apply_static_peers_added_to_dynamic_are_unchanged() {
1469		let [s1, s2] = fresh_peers();
1470		let (connected_no_slot, num_in) = run_apply(
1471			vec![(s1, true), (s2, true)],
1472			set_of([s1, s2]),
1473			HashSet::new(),
1474			set_of([s1, s2]),
1475			set_of([s1, s2]),
1476			4,
1477		);
1478		assert_eq!(connected_no_slot, set_of([s1, s2]));
1479		assert_eq!(num_in, 4);
1480	}
1481
1482	#[test]
1483	fn apply_unconnected_peers_in_new_set_are_ignored() {
1484		// Unconnected peers go into `dynamic_no_slot_peers` (caller-installed) and take effect
1485		// on connect; they must not appear in `connected_no_slot` here.
1486		let [connected_a, connected_b] = fresh_peers();
1487		let [unconnected_a, unconnected_b] = fresh_peers();
1488		let (connected_no_slot, num_in) = run_apply(
1489			vec![(connected_a, true), (connected_b, true)],
1490			HashSet::new(),
1491			HashSet::new(),
1492			set_of([unconnected_a, unconnected_b]),
1493			HashSet::new(),
1494			3,
1495		);
1496		assert!(connected_no_slot.is_empty());
1497		assert_eq!(num_in, 3);
1498	}
1499
1500	#[test]
1501	fn apply_idempotent_same_set() {
1502		let [in_full, out_full, light] = fresh_peers();
1503		let target = set_of([in_full, out_full, light]);
1504		let (connected_no_slot, num_in) = run_apply(
1505			vec![(in_full, true), (out_full, false), (light, false)],
1506			HashSet::new(),
1507			target.clone(),
1508			target.clone(),
1509			target.clone(),
1510			2,
1511		);
1512		assert_eq!(connected_no_slot, target);
1513		assert_eq!(num_in, 2);
1514	}
1515
1516	#[test]
1517	fn apply_empty_set_clears_dynamic_only_peers() {
1518		let [in1, in2, out, light] = fresh_peers();
1519		let [static_peer] = fresh_peers();
1520		let old = set_of([in1, in2, out, light, static_peer]);
1521		let (connected_no_slot, num_in) = run_apply(
1522			vec![(in1, true), (in2, true), (out, false), (light, false), (static_peer, true)],
1523			set_of([static_peer]),
1524			old.clone(),
1525			HashSet::new(),
1526			old,
1527			0,
1528		);
1529		assert_eq!(connected_no_slot, set_of([static_peer]));
1530		assert_eq!(num_in, 2);
1531	}
1532
1533	#[test]
1534	fn apply_mixed_promote_and_demote() {
1535		let [p1, p2] = fresh_peers();
1536		let [d1, d2] = fresh_peers();
1537		let (connected_no_slot, num_in) = run_apply(
1538			vec![(p1, true), (p2, true), (d1, true), (d2, true)],
1539			HashSet::new(),
1540			set_of([d1, d2]),
1541			set_of([p1, p2]),
1542			set_of([d1, d2]),
1543			5,
1544		);
1545		assert_eq!(connected_no_slot, set_of([p1, p2]));
1546		assert_eq!(num_in, 5);
1547	}
1548
1549	#[test]
1550	fn apply_demote_at_capacity_disconnects_peer() {
1551		// Scenario: PeerX was promoted (freeing a slot), then a regular PeerY filled that slot,
1552		// bringing `num_in_peers` back to capacity. Now PeerX is demoted out of the dynamic set
1553		// โ€” incrementing `num_in_peers` would push it strictly above `max_in_peers`. The peer
1554		// must be disconnected instead, and left in `connected_no_slot` so the async disconnect
1555		// handler is the sole updater of `num_in_peers`.
1556		let [px] = fresh_peers();
1557		let (connected_no_slot, num_in, disconnects) = run_apply_with_cap(
1558			vec![(px, true)],
1559			HashSet::new(),
1560			set_of([px]),
1561			HashSet::new(),
1562			set_of([px]),
1563			8,
1564			8,
1565		);
1566		assert_eq!(connected_no_slot, set_of([px]));
1567		assert_eq!(num_in, 8);
1568		assert_eq!(disconnects, vec![px]);
1569	}
1570
1571	#[test]
1572	fn apply_demote_below_capacity_increments_normally() {
1573		// Same shape as the over-capacity test but with `num_in_peers < max_in_peers`: the
1574		// peer is regularly demoted and `num_in_peers` is incremented.
1575		let [px] = fresh_peers();
1576		let (connected_no_slot, num_in, disconnects) = run_apply_with_cap(
1577			vec![(px, true)],
1578			HashSet::new(),
1579			set_of([px]),
1580			HashSet::new(),
1581			set_of([px]),
1582			7,
1583			8,
1584		);
1585		assert!(connected_no_slot.is_empty());
1586		assert_eq!(num_in, 8);
1587		assert!(disconnects.is_empty());
1588	}
1589
1590	#[test]
1591	fn num_connected_gauge_tracks_the_shared_counter() {
1592		let registry = Registry::new();
1593		let num_connected = Arc::new(AtomicUsize::new(0));
1594		NumConnectedGauge::register(&registry, num_connected.clone()).unwrap();
1595
1596		let peers_count = || {
1597			registry
1598				.gather()
1599				.iter()
1600				.find(|family| family.get_name() == "substrate_sub_libp2p_peers_count")
1601				.map(|family| family.get_metric()[0].get_gauge().get_value())
1602		};
1603
1604		assert_eq!(peers_count(), Some(0.0));
1605
1606		num_connected.store(3, Ordering::Relaxed);
1607		assert_eq!(peers_count(), Some(3.0));
1608
1609		num_connected.store(0, Ordering::Relaxed);
1610		assert_eq!(peers_count(), Some(0.0));
1611	}
1612}