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