Skip to main content

sc_network_bitswap/
service.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Substrate.
3// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
4
5// Substrate is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9
10// Substrate is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14
15// You should have received a copy of the GNU General Public License
16// along with Substrate. If not, see <https://www.gnu.org/licenses/>.
17
18//! Bitswap service for indexed transactions.
19//!
20//! Inbound requests are resolved against local storage and returned to the requesting peer.
21//! Outbound requests are scheduled across connected peers and delivered through the user-facing
22//! response stream.
23//!
24//! Both paths share one actor, which coordinates network events, request limits, retries, and
25//! backpressure.
26
27use super::{
28	is_cid_supported, BitswapCommand, BitswapHandle, Cid, FetchItem, LOG_TARGET, MAX_WANTED_BLOCKS,
29};
30use crate::{
31	handle::BitswapError,
32	metrics::{
33		errors as metric_errors, outbound_events, outcomes as metric_outcomes, BitswapMetrics,
34	},
35};
36
37use async_trait::async_trait;
38use bytes::Bytes;
39use futures::{Stream, StreamExt};
40use litep2p::protocol::libp2p::bitswap::{
41	BitswapEvent, BitswapHandle as Litep2pBitswapHandle, BlockPresenceType, ResponseType, WantType,
42};
43use prometheus_endpoint::Registry;
44use rand::{seq::IteratorRandom, Rng};
45use sc_client_api::BlockBackend;
46use sc_network_common::role::Roles;
47use sc_network_sync::{SyncEvent, SyncEventStream};
48use slotmap::{new_key_type, SlotMap};
49use smallvec::SmallVec;
50use sp_core::H256;
51use sp_runtime::traits::Block as BlockT;
52use std::{
53	collections::{hash_map::Entry, HashMap, HashSet, VecDeque},
54	future::Future,
55	pin::Pin,
56	sync::Arc,
57	time::Duration,
58};
59use tokio::{
60	sync::{mpsc, OwnedSemaphorePermit, Semaphore},
61	time::Instant,
62};
63
64/// Transport boundary used by the Bitswap actor.
65/// Implementations must verify block bytes before emitting `VerifiedBlock`.
66#[async_trait]
67trait BitswapTransport: Send {
68	async fn next_event(&mut self) -> Option<TransportEvent>;
69	async fn send_request(&self, peer: litep2p::PeerId, cids: Vec<(Cid, WantType)>);
70	async fn send_response(&self, peer: litep2p::PeerId, responses: Vec<ResponseType>);
71}
72
73enum TransportEvent {
74	Request { peer: litep2p::PeerId, cids: Vec<(Cid, WantType)> },
75	Response { peer: litep2p::PeerId, responses: Vec<TransportResponse> },
76}
77
78enum TransportResponse {
79	/// A block whose CID is guaranteed by the transport to match `bytes`. litep2p derives
80	/// the CID from the received bytes (the wire format only carries a CID prefix), so
81	/// the pairing needs no further verification; any other transport must uphold this.
82	VerifiedBlock {
83		cid: Cid,
84		bytes: Bytes,
85	},
86	Presence {
87		cid: Cid,
88		presence: BlockPresenceType,
89	},
90}
91
92/// Sends are bounded by [`TRANSPORT_SEND_TIMEOUT`]: the channels in both directions are
93/// bounded and litep2p blocks when the event channel towards us is full, so unbounded sends
94/// on our side could deadlock both tasks if both channels ever fill up simultaneously.
95/// Dropping is safe: wants are retried and remote peers re-request lost responses.
96#[async_trait]
97impl BitswapTransport for Litep2pBitswapHandle {
98	async fn next_event(&mut self) -> Option<TransportEvent> {
99		StreamExt::next(self).await.map(|event| match event {
100			BitswapEvent::Request { peer, cids } => TransportEvent::Request { peer, cids },
101			BitswapEvent::Response { peer, responses } => TransportEvent::Response {
102				peer,
103				responses: responses
104					.into_iter()
105					.map(|response| match response {
106						ResponseType::Block { cid, block } => {
107							TransportResponse::VerifiedBlock { cid, bytes: block.into() }
108						},
109						ResponseType::Presence { cid, presence } => {
110							TransportResponse::Presence { cid, presence }
111						},
112					})
113					.collect(),
114			},
115		})
116	}
117
118	async fn send_request(&self, peer: litep2p::PeerId, cids: Vec<(Cid, WantType)>) {
119		let send = Litep2pBitswapHandle::send_request(self, peer, cids);
120		if tokio::time::timeout(TRANSPORT_SEND_TIMEOUT, send).await.is_err() {
121			log::warn!(
122				target: LOG_TARGET,
123				"litep2p command channel congested; dropped bitswap request to {peer:?}",
124			);
125		}
126	}
127
128	async fn send_response(&self, peer: litep2p::PeerId, responses: Vec<ResponseType>) {
129		let send = Litep2pBitswapHandle::send_response(self, peer, responses);
130		if tokio::time::timeout(TRANSPORT_SEND_TIMEOUT, send).await.is_err() {
131			log::warn!(
132				target: LOG_TARGET,
133				"litep2p command channel congested; dropped bitswap response to {peer:?}",
134			);
135		}
136	}
137}
138
139const MAX_LIVE_CIDS: usize = 1024;
140const MAX_CONCURRENT_INBOUND_LOOKUPS: usize = 8;
141const MAX_QUEUED_INBOUND_ENTRIES_PER_PEER: usize = MAX_LIVE_CIDS;
142const CMD_CHANNEL_CAPACITY: usize = 256;
143/// Upper bound on one send into the litep2p command channel; see the
144/// [`BitswapTransport`] impl for [`Litep2pBitswapHandle`].
145const TRANSPORT_SEND_TIMEOUT: Duration = Duration::from_secs(10);
146const PER_PEER_TIMEOUT: Duration = Duration::from_secs(5);
147const ROUND_RETRY_DELAY: Duration = Duration::from_secs(5);
148const SWEEP_INTERVAL: Duration = Duration::from_secs(1);
149
150new_key_type! { struct UserRequestId; }
151
152#[derive(Clone, Copy, Default)]
153enum CidRequestPhase {
154	#[default]
155	Ready,
156	Queued {
157		retry_at: Option<Instant>,
158	},
159	InFlight {
160		peer: litep2p::PeerId,
161		deadline: Instant,
162	},
163	RetryAt(Instant),
164}
165
166#[derive(Default)]
167struct CidRequestState {
168	tried_peers: HashSet<litep2p::PeerId>,
169	user_requests: SmallVec<[UserRequestId; 2]>,
170	phase: CidRequestPhase,
171}
172
173impl CidRequestState {
174	fn has_user_requests(&self) -> bool {
175		!self.user_requests.is_empty()
176	}
177
178	fn is_idle(&self) -> bool {
179		self.user_requests.is_empty() && !matches!(self.phase, CidRequestPhase::InFlight { .. })
180	}
181
182	fn is_queued(&self) -> bool {
183		matches!(self.phase, CidRequestPhase::Queued { .. })
184	}
185
186	/// Moves a ready CID into the pending queue while preserving its retry deadline.
187	/// Returns `false` when the CID is already queued or in flight.
188	fn queue(&mut self) -> bool {
189		let retry_at = match self.phase {
190			CidRequestPhase::Ready => None,
191			CidRequestPhase::RetryAt(at) => Some(at),
192			CidRequestPhase::Queued { .. } | CidRequestPhase::InFlight { .. } => return false,
193		};
194		self.phase = CidRequestPhase::Queued { retry_at };
195		true
196	}
197
198	/// Removes a CID from the pending queue and restores its prior scheduling phase.
199	/// Returns `false` when the state was not queued.
200	fn dequeue(&mut self) -> bool {
201		let CidRequestPhase::Queued { retry_at } = self.phase else { return false };
202		self.phase = retry_at.map_or(CidRequestPhase::Ready, CidRequestPhase::RetryAt);
203		true
204	}
205
206	/// Finishes the active request only when `peer` currently owns it.
207	/// The return value indicates whether an in-flight slot was released.
208	fn finish_peer(&mut self, peer: litep2p::PeerId) -> bool {
209		let CidRequestPhase::InFlight { peer: active_peer, .. } = self.phase else { return false };
210		if active_peer != peer {
211			return false;
212		}
213		self.phase = CidRequestPhase::Ready;
214		true
215	}
216
217	/// Parks an exhausted CID until `at`, including while it waits in the queue.
218	/// Returns `false` when a retry is already scheduled or work remains in flight.
219	fn schedule_retry(&mut self, at: Instant) -> bool {
220		match &mut self.phase {
221			CidRequestPhase::Ready => self.phase = CidRequestPhase::RetryAt(at),
222			CidRequestPhase::Queued { retry_at: slot @ None } => *slot = Some(at),
223			CidRequestPhase::Queued { retry_at: Some(_) } |
224			CidRequestPhase::InFlight { .. } |
225			CidRequestPhase::RetryAt(_) => return false,
226		}
227		true
228	}
229
230	/// Starts a new peer-selection round once the retry deadline has elapsed.
231	/// Peer history is cleared so every connected peer becomes eligible again.
232	fn restart_round(&mut self, now: Instant) -> bool {
233		self.phase = match self.phase {
234			CidRequestPhase::RetryAt(at) if at <= now => CidRequestPhase::Ready,
235			CidRequestPhase::Queued { retry_at: Some(at) } if at <= now => {
236				CidRequestPhase::Queued { retry_at: None }
237			},
238			_ => return false,
239		};
240		self.tried_peers.clear();
241		true
242	}
243}
244
245struct RequestScheduler {
246	cid_states: HashMap<Cid, CidRequestState>,
247	/// FIFO of CIDs waiting for a free dispatch-window slot. May contain stale entries
248	/// (resolved, abandoned or already-dispatched CIDs); those are skipped on pop, guarded
249	/// by [`CidRequestState::is_queued`].
250	pending: VecDeque<Cid>,
251	/// Number of CIDs with at least one in-flight peer request.
252	live_cids: usize,
253	/// Number of queued [`CidRequestState`] entries, maintained incrementally: the
254	/// metrics path reads it after every actor event, so recomputing it by scanning
255	/// `cid_states` would make large requests quadratic.
256	queued_cids: usize,
257	/// Dispatch-window size.
258	max_live_cids: usize,
259}
260
261impl RequestScheduler {
262	fn new(max_live_cids: usize) -> Self {
263		Self {
264			cid_states: HashMap::new(),
265			pending: VecDeque::new(),
266			live_cids: 0,
267			queued_cids: 0,
268			max_live_cids,
269		}
270	}
271
272	fn contains(&self, cid: &Cid) -> bool {
273		self.cid_states.contains_key(cid)
274	}
275
276	fn add_user_request(&mut self, cid: Cid, user_request: UserRequestId) {
277		self.cid_states.entry(cid).or_default().user_requests.push(user_request);
278	}
279
280	/// Detaches a user request from `cid` and removes newly idle state.
281	/// In-flight state remains until its response, timeout, or disconnect arrives.
282	fn remove_user_request(&mut self, cid: Cid, user_request: UserRequestId) {
283		if let Some(cid_state) = self.cid_states.get_mut(&cid) {
284			cid_state.user_requests.retain(|r| *r != user_request);
285		}
286		self.remove_if_idle(cid);
287	}
288
289	fn all_cids(&self) -> Vec<Cid> {
290		self.cid_states.keys().copied().collect()
291	}
292
293	/// Removes a delivered CID and returns every user request that should receive it.
294	/// Scheduler counters are released according to the CID's previous phase.
295	fn take_user_requests_for_delivered_cid(
296		&mut self,
297		cid: Cid,
298	) -> Option<SmallVec<[UserRequestId; 2]>> {
299		self.cid_states.remove(&cid).map(|cid_state| {
300			if matches!(cid_state.phase, CidRequestPhase::InFlight { .. }) {
301				self.live_cids -= 1;
302			}
303			if cid_state.is_queued() {
304				self.queued_cids -= 1;
305			}
306			cid_state.user_requests
307		})
308	}
309
310	fn has_window_capacity(&self) -> bool {
311		self.live_cids < self.max_live_cids
312	}
313
314	/// Pops the next still-valid queued CID and updates its queue accounting.
315	/// Stale FIFO entries are skipped until a queued state is found.
316	fn pop_pending(&mut self) -> Option<Cid> {
317		while let Some(cid) = self.pending.pop_front() {
318			if let Some(cid_state) = self.cid_states.get_mut(&cid) {
319				if cid_state.dequeue() {
320					self.queued_cids -= 1;
321					return Some(cid);
322				}
323			}
324		}
325		None
326	}
327
328	/// Selects an eligible peer for `cid`, queueing it when the dispatch window is full.
329	/// A selected peer owns one live slot until completion, timeout, or disconnect.
330	fn next_peer_to_request<R: Rng + ?Sized>(
331		&mut self,
332		cid: Cid,
333		connected_peers: &HashSet<litep2p::PeerId>,
334		now: Instant,
335		rng: &mut R,
336	) -> Option<litep2p::PeerId> {
337		let has_window_capacity = self.has_window_capacity();
338		let cid_state = self.cid_states.get_mut(&cid)?;
339		if !cid_state.has_user_requests() ||
340			matches!(cid_state.phase, CidRequestPhase::InFlight { .. })
341		{
342			return None;
343		}
344
345		if !has_window_capacity {
346			// Preserve the CID for promotion when a dispatch slot opens.
347			if cid_state.queue() {
348				self.pending.push_back(cid);
349				self.queued_cids += 1;
350			}
351			return None;
352		}
353
354		let is_untried = |peer: &litep2p::PeerId| !cid_state.tried_peers.contains(peer);
355		let Some(peer) = connected_peers
356			.iter()
357			.filter(|peer| is_untried(peer))
358			.choose(&mut *rng)
359			.copied()
360		else {
361			if !connected_peers.is_empty() {
362				// Retry later once every connected peer has been tried.
363				if cid_state.schedule_retry(now + ROUND_RETRY_DELAY) {
364					log::trace!(
365						target: LOG_TARGET,
366						"all peers tried for {cid}, scheduling new round",
367					);
368				}
369			}
370			return None;
371		};
372
373		if cid_state.is_queued() {
374			self.queued_cids -= 1;
375		}
376		self.live_cids += 1;
377		cid_state.phase = CidRequestPhase::InFlight { peer, deadline: now + PER_PEER_TIMEOUT };
378
379		Some(peer)
380	}
381
382	/// Records a terminal response from `peer` and releases its active slot.
383	/// Late responses cannot finish a newer request owned by a different peer.
384	fn mark_peer_done_for_cid(&mut self, peer: litep2p::PeerId, cid: Cid) {
385		if let Some(cid_state) = self.cid_states.get_mut(&cid) {
386			if cid_state.finish_peer(peer) {
387				self.live_cids -= 1;
388			}
389			cid_state.tried_peers.insert(peer);
390		}
391		self.remove_if_idle(cid);
392	}
393
394	/// Releases every active request owned by a disconnected peer.
395	/// Returned CIDs are still wanted and ready for immediate failover.
396	fn remove_in_flight_peer(&mut self, peer: litep2p::PeerId) -> Vec<Cid> {
397		let mut affected_cids = Vec::new();
398		for (cid, cid_state) in self.cid_states.iter_mut() {
399			if cid_state.finish_peer(peer) {
400				self.live_cids -= 1;
401				affected_cids.push(*cid);
402			}
403		}
404
405		self.remove_idle_and_filter_existing(affected_cids)
406	}
407
408	/// Expire in-flight requests whose per-peer deadline passed. Returns the affected CIDs
409	/// still wanted (for re-dispatch) and the total number of timed-out peer requests.
410	fn expire_peer_timeouts(&mut self, now: Instant) -> (Vec<Cid>, usize) {
411		let mut timed_out_cids = Vec::new();
412		for (cid, cid_state) in self.cid_states.iter_mut() {
413			if let CidRequestPhase::InFlight { peer, deadline } = cid_state.phase {
414				if deadline <= now {
415					cid_state.phase = CidRequestPhase::Ready;
416					cid_state.tried_peers.insert(peer);
417					timed_out_cids.push(*cid);
418				}
419			}
420		}
421		let timed_out_count = timed_out_cids.len();
422		self.live_cids -= timed_out_count;
423
424		(self.remove_idle_and_filter_existing(timed_out_cids), timed_out_count)
425	}
426
427	/// Restarts every exhausted CID whose retry delay has elapsed.
428	/// Returned CIDs should be reconsidered for immediate dispatch.
429	fn restart_exhausted_rounds(&mut self, now: Instant) -> Vec<Cid> {
430		let mut restarted_cids = Vec::new();
431		for (cid, cid_state) in self.cid_states.iter_mut() {
432			if cid_state.has_user_requests() && cid_state.restart_round(now) {
433				restarted_cids.push(*cid);
434			}
435		}
436		restarted_cids
437	}
438
439	fn clear(&mut self) {
440		self.cid_states.clear();
441		self.pending.clear();
442		self.live_cids = 0;
443		self.queued_cids = 0;
444	}
445
446	/// Removes idle states from `cids` and returns those still tracked.
447	/// The filtered result is safe to feed back into peer selection.
448	fn remove_idle_and_filter_existing(&mut self, cids: Vec<Cid>) -> Vec<Cid> {
449		for cid in &cids {
450			self.remove_if_idle(*cid);
451		}
452		cids.into_iter().filter(|cid| self.cid_states.contains_key(cid)).collect()
453	}
454
455	fn remove_if_idle(&mut self, cid: Cid) {
456		let Entry::Occupied(entry) = self.cid_states.entry(cid) else { return };
457		if !entry.get().is_idle() {
458			return;
459		}
460		let cid_state = entry.remove();
461		if cid_state.is_queued() {
462			self.queued_cids -= 1;
463		}
464	}
465}
466
467struct UserRequest {
468	cids_remaining: HashSet<Cid>,
469	sink: mpsc::Sender<FetchItem>,
470}
471
472/// A served batch, carrying the worker permit: the slot is released only once the actor
473/// has forwarded the responses, so downstream backpressure reaches the dispatch path and
474/// at most [`MAX_CONCURRENT_INBOUND_LOOKUPS`] responses are retained at any time.
475type InboundLookupResult = (litep2p::PeerId, Vec<ResponseType>, OwnedSemaphorePermit);
476
477struct InboundLookupPool<B: BlockT> {
478	client: Arc<dyn BlockBackend<B> + Send + Sync>,
479	result_tx: mpsc::Sender<InboundLookupResult>,
480	semaphore: Arc<Semaphore>,
481	metrics: BitswapMetrics,
482}
483
484impl<B: BlockT> InboundLookupPool<B> {
485	fn new(
486		client: Arc<dyn BlockBackend<B> + Send + Sync>,
487		max_lookups: usize,
488		metrics: BitswapMetrics,
489	) -> (Self, mpsc::Receiver<InboundLookupResult>) {
490		let (result_tx, result_rx) = mpsc::channel(max_lookups);
491		(
492			Self { client, result_tx, semaphore: Arc::new(Semaphore::new(max_lookups)), metrics },
493			result_rx,
494		)
495	}
496
497	/// Reserves a worker slot, or returns `None` if all workers are busy.
498	/// Dropping an unused permit immediately returns the slot.
499	fn try_acquire_worker(&self) -> Option<OwnedSemaphorePermit> {
500		self.semaphore.clone().try_acquire_owned().ok()
501	}
502
503	/// Serves a wantlist on a blocking worker occupying `permit`'s slot.
504	/// The permit travels with the result so backpressure retains the slot.
505	fn submit(
506		&self,
507		permit: OwnedSemaphorePermit,
508		peer: litep2p::PeerId,
509		cids: Vec<(Cid, WantType)>,
510	) {
511		let client = self.client.clone();
512		let result_tx = self.result_tx.clone();
513		let metrics = self.metrics.clone();
514		tokio::task::spawn_blocking(move || {
515			let responses = serve_inbound(&*client, cids, &metrics);
516			let _ = result_tx.blocking_send((peer, responses, permit));
517		});
518	}
519}
520
521/// Fair per-peer queues. Overflow is dropped rather than reported as `DONT_HAVE`.
522struct InboundQueue {
523	per_peer: HashMap<litep2p::PeerId, VecDeque<(Cid, WantType)>>,
524	rotation: VecDeque<litep2p::PeerId>,
525	max_entries_per_peer: usize,
526}
527
528impl InboundQueue {
529	fn new(max_entries_per_peer: usize) -> Self {
530		Self { per_peer: HashMap::new(), rotation: VecDeque::new(), max_entries_per_peer }
531	}
532
533	/// Queue `entries` for `peer`. Returns the number of entries dropped because the
534	/// peer's queue is full.
535	fn enqueue(&mut self, peer: litep2p::PeerId, mut entries: Vec<(Cid, WantType)>) -> usize {
536		if entries.is_empty() {
537			return 0;
538		}
539		let queue = self.per_peer.entry(peer).or_default();
540		let free_slots = self.max_entries_per_peer.saturating_sub(queue.len());
541		let dropped = entries.len().saturating_sub(free_slots);
542		entries.truncate(free_slots);
543		// Enter the rotation only on the empty-to-nonempty transition.
544		if queue.is_empty() && !entries.is_empty() {
545			self.rotation.push_back(peer);
546		}
547		queue.extend(entries);
548		if queue.is_empty() {
549			self.per_peer.remove(&peer);
550		}
551		dropped
552	}
553
554	/// Drops all queued entries of a peer, returning how many were removed.
555	fn remove_peer(&mut self, peer: &litep2p::PeerId) -> usize {
556		let removed = self.per_peer.remove(peer).map_or(0, |queue| queue.len());
557		if removed > 0 {
558			self.rotation.retain(|rotated| rotated != peer);
559		}
560		removed
561	}
562
563	/// Take the next batch of up to [`MAX_WANTED_BLOCKS`] entries, rotating the serviced
564	/// peer to the back of the queue.
565	fn next_batch(&mut self) -> Option<(litep2p::PeerId, Vec<(Cid, WantType)>)> {
566		while let Some(peer) = self.rotation.pop_front() {
567			let Some(queue) = self.per_peer.get_mut(&peer) else {
568				log::error!(target: LOG_TARGET, "stale peer in inbound queue rotation: {peer}");
569				continue;
570			};
571			if queue.is_empty() {
572				log::error!(target: LOG_TARGET, "empty peer queue in inbound rotation: {peer}");
573				self.per_peer.remove(&peer);
574				continue;
575			}
576
577			let batch: Vec<_> = queue.drain(..queue.len().min(MAX_WANTED_BLOCKS)).collect();
578			if queue.is_empty() {
579				self.per_peer.remove(&peer);
580			} else {
581				self.rotation.push_back(peer);
582			}
583			return Some((peer, batch));
584		}
585		None
586	}
587}
588
589pub(crate) struct BitswapService<B: BlockT> {
590	handle: Box<dyn BitswapTransport>,
591
592	cmd_rx: mpsc::Receiver<BitswapCommand>,
593	cmd_channel_closed: bool,
594	sync_event_stream: Pin<Box<dyn Stream<Item = SyncEvent> + Send>>,
595	/// Runs blocking local transaction lookups with bounded concurrency.
596	inbound_lookup_pool: InboundLookupPool<B>,
597	inbound_lookup_rx: mpsc::Receiver<InboundLookupResult>,
598	/// Buffers inbound wantlist entries fairly per peer until a lookup worker is available.
599	inbound_queue: InboundQueue,
600
601	connected_peers: HashSet<litep2p::PeerId>,
602	/// Coordinates each requested CID across user requests, peers, retries, and dispatch slots.
603	scheduler: RequestScheduler,
604	/// Active user-facing requests that receive blocks resolved by the scheduler.
605	user_requests: SlotMap<UserRequestId, UserRequest>,
606	metrics: BitswapMetrics,
607}
608
609/// Build the Bitswap service, returning the service future and the user-facing handle.
610pub fn start<B: BlockT, S>(
611	client: Arc<dyn BlockBackend<B> + Send + Sync>,
612	sync: &S,
613	litep2p_handle: Litep2pBitswapHandle,
614	metrics_registry: Option<&Registry>,
615) -> (Pin<Box<dyn Future<Output = ()> + Send>>, BitswapHandle)
616where
617	S: SyncEventStream + ?Sized,
618{
619	let (cmd_tx, cmd_rx) = mpsc::channel(CMD_CHANNEL_CAPACITY);
620	let metrics = BitswapMetrics::new(metrics_registry).unwrap_or_else(|err| {
621		log::debug!(target: LOG_TARGET, "failed to register bitswap metrics: {err}");
622		BitswapMetrics::default()
623	});
624	let (inbound_lookup_pool, inbound_lookup_rx) =
625		InboundLookupPool::new(client, MAX_CONCURRENT_INBOUND_LOOKUPS, metrics.clone());
626
627	let user_handle = BitswapHandle::new(cmd_tx);
628	let sync_event_stream = sync.event_stream("bitswap");
629
630	let service = BitswapService {
631		handle: Box::new(litep2p_handle),
632		cmd_rx,
633		cmd_channel_closed: false,
634		sync_event_stream,
635		inbound_lookup_pool,
636		inbound_lookup_rx,
637		inbound_queue: InboundQueue::new(MAX_QUEUED_INBOUND_ENTRIES_PER_PEER),
638		connected_peers: HashSet::new(),
639		scheduler: RequestScheduler::new(MAX_LIVE_CIDS),
640		user_requests: SlotMap::with_key(),
641		metrics,
642	};
643
644	let future = Box::pin(async move { service.run().await });
645
646	(future, user_handle)
647}
648
649impl<B: BlockT> BitswapService<B> {
650	/// Runs the service actor until a required input stream closes.
651	/// Each event is handled serially before metrics are refreshed.
652	async fn run(mut self) {
653		log::debug!(target: LOG_TARGET, "BitswapService starting");
654		let mut sweep_ticker = tokio::time::interval(SWEEP_INTERVAL);
655		sweep_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
656		sweep_ticker.tick().await;
657		self.update_metrics();
658
659		loop {
660			tokio::select! {
661				event = self.handle.next_event() => match event {
662					Some(TransportEvent::Request { peer, cids }) =>
663						self.on_inbound_request(peer, cids),
664					Some(TransportEvent::Response { peer, responses }) =>
665						self.on_inbound_response(peer, responses).await,
666					None => {
667						log::debug!(target: LOG_TARGET, "litep2p bitswap stream ended; shutting down");
668						self.shutdown_user_requests();
669						return;
670					},
671				},
672
673				cmd = self.cmd_rx.recv(), if !self.cmd_channel_closed => match cmd {
674					Some(BitswapCommand::RequestStream { cids, sink }) =>
675						self.on_request_stream(cids, sink).await,
676					None => {
677						log::debug!(
678							target: LOG_TARGET,
679							"all bitswap handles dropped; serving inbound requests only",
680						);
681						self.cmd_channel_closed = true;
682					},
683				},
684
685				sync_event = self.sync_event_stream.next() => match sync_event {
686					Some(SyncEvent::PeerConnected { peer_id, roles }) =>
687						self.on_peer_connected(peer_id.into(), roles).await,
688					Some(SyncEvent::PeerDisconnected(peer)) => self.on_peer_disconnected(peer.into()).await,
689					None => {
690						log::debug!(target: LOG_TARGET, "sync event stream ended; shutting down");
691						self.shutdown_user_requests();
692						return;
693					},
694				},
695
696				Some((peer, responses, permit)) = self.inbound_lookup_rx.recv() => {
697					self.handle.send_response(peer, responses).await;
698					drop(permit);
699					self.dispatch_inbound_lookups();
700				},
701
702				_ = sweep_ticker.tick() => {
703					self.on_sweep().await;
704				},
705			}
706			self.update_metrics();
707		}
708	}
709
710	/// Admits a user wantlist and attaches one user request to all requested CIDs.
711	async fn on_request_stream(&mut self, cids: HashSet<Cid>, sink: mpsc::Sender<FetchItem>) {
712		let user_request_id =
713			self.user_requests.insert(UserRequest { cids_remaining: cids.clone(), sink });
714
715		for cid in &cids {
716			self.scheduler.add_user_request(*cid, user_request_id);
717		}
718
719		self.top_up_in_flight(cids).await;
720	}
721
722	/// Schedules the supplied CIDs, then promotes queued CIDs until the dispatch window is full.
723	/// Eligible wants are grouped by peer and sent in protocol-sized batches.
724	async fn top_up_in_flight(&mut self, cids: impl IntoIterator<Item = Cid>) {
725		let now = Instant::now();
726		let wants_by_peer = {
727			let mut rng = rand::thread_rng();
728			let mut wants_by_peer: HashMap<litep2p::PeerId, Vec<(Cid, WantType)>> = HashMap::new();
729			for cid in cids {
730				if let Some(peer) =
731					self.scheduler.next_peer_to_request(cid, &self.connected_peers, now, &mut rng)
732				{
733					log::trace!(target: LOG_TARGET, "WANT-BLOCK {cid} -> {peer:?}");
734					wants_by_peer.entry(peer).or_default().push((cid, WantType::Block));
735				}
736			}
737
738			// Fill remaining dispatch slots from the pending FIFO.
739			while self.scheduler.has_window_capacity() {
740				let Some(cid) = self.scheduler.pop_pending() else { break };
741				if let Some(peer) =
742					self.scheduler.next_peer_to_request(cid, &self.connected_peers, now, &mut rng)
743				{
744					log::trace!(target: LOG_TARGET, "WANT-BLOCK {cid} -> {peer:?} (promoted)");
745					wants_by_peer.entry(peer).or_default().push((cid, WantType::Block));
746				}
747			}
748			wants_by_peer
749		};
750
751		for (peer, wants) in wants_by_peer {
752			self.metrics.record_outbound(outbound_events::REQUESTED, wants.len());
753			for chunk in wants.chunks(MAX_WANTED_BLOCKS) {
754				self.handle.send_request(peer, chunk.to_vec()).await;
755			}
756		}
757	}
758
759	/// Applies verified blocks and presence updates received from a peer.
760	/// Unresolved presence responses are immediately reconsidered for dispatch.
761	async fn on_inbound_response(
762		&mut self,
763		peer: litep2p::PeerId,
764		responses: Vec<TransportResponse>,
765	) {
766		let mut cids_to_top_up: HashSet<Cid> = HashSet::new();
767
768		for response in responses {
769			match response {
770				TransportResponse::VerifiedBlock { cid, bytes } => {
771					// A late verified block may satisfy a want reassigned to another peer.
772					self.scheduler.mark_peer_done_for_cid(peer, cid);
773
774					if self.scheduler.contains(&cid) {
775						self.deliver_block(cid, bytes);
776					} else {
777						log::debug!(
778							target: LOG_TARGET,
779							"{peer:?} sent unsolicited or unwanted block for {cid}",
780						);
781					}
782				},
783				TransportResponse::Presence { cid, presence } => match presence {
784					BlockPresenceType::DontHave => {
785						log::trace!(target: LOG_TARGET, "{peer:?} DONT_HAVE {cid}");
786						self.scheduler.mark_peer_done_for_cid(peer, cid);
787						cids_to_top_up.insert(cid);
788					},
789					BlockPresenceType::Have => {
790						log::debug!(
791							target: LOG_TARGET,
792							"{peer:?} sent unsolicited HAVE for {cid}; only WANT-BLOCK is issued",
793						);
794					},
795				},
796			}
797		}
798
799		self.top_up_in_flight(cids_to_top_up).await;
800	}
801
802	/// Delivers a resolved block to every user request sharing its CID.
803	/// The CID is removed once, releasing any replacement in-flight request.
804	fn deliver_block(&mut self, cid: Cid, bytes: Bytes) {
805		let Some(user_request_ids) = self.scheduler.take_user_requests_for_delivered_cid(cid)
806		else {
807			return;
808		};
809		self.metrics.record_outbound(outbound_events::DELIVERED, 1);
810
811		for user_request_id in user_request_ids {
812			let Some(user_request) = self.user_requests.get_mut(user_request_id) else { continue };
813			if !user_request.cids_remaining.remove(&cid) {
814				continue;
815			}
816			if user_request.sink.try_send(Ok((cid, bytes.clone()))).is_err() {
817				log::trace!(target: LOG_TARGET, "user request sink full/closed for {cid}");
818				self.drop_user_request(user_request_id);
819				continue;
820			}
821			if user_request.cids_remaining.is_empty() {
822				self.drop_user_request(user_request_id);
823			}
824		}
825	}
826
827	/// Removes a user request and detaches it from every unresolved CID.
828	/// CID state is retained only while another user request or peer request needs it.
829	fn drop_user_request(&mut self, id: UserRequestId) {
830		let Some(user_request) = self.user_requests.remove(id) else { return };
831
832		for cid in &user_request.cids_remaining {
833			self.scheduler.remove_user_request(*cid, id);
834		}
835	}
836
837	/// Adds a connected non-light peer and reconsiders every outstanding CID.
838	/// Light peers are ignored because they do not store indexed transactions.
839	async fn on_peer_connected(&mut self, peer: litep2p::PeerId, roles: Roles) {
840		if roles.is_light() {
841			return;
842		}
843		self.connected_peers.insert(peer);
844		let cids = self.scheduler.all_cids();
845		self.top_up_in_flight(cids).await;
846	}
847
848	/// Removes a peer and immediately fails over requests it owned.
849	/// Late responses remain safe because peer ownership is checked on completion.
850	/// Queued inbound wantlist entries of the peer are dropped so departed peers
851	/// neither occupy queue capacity nor waste lookup work.
852	async fn on_peer_disconnected(&mut self, peer: litep2p::PeerId) {
853		self.connected_peers.remove(&peer);
854		let dropped = self.inbound_queue.remove_peer(&peer);
855		if dropped > 0 {
856			log::trace!(
857				target: LOG_TARGET,
858				"dropped {dropped} queued inbound entries of disconnected {peer:?}",
859			);
860		}
861		let cids_to_top_up = self.scheduler.remove_in_flight_peer(peer);
862		self.top_up_in_flight(cids_to_top_up).await;
863	}
864
865	/// Periodic housekeeping: drop user requests whose receiver was dropped, expire per-peer
866	/// request timeouts, and start new rounds for CIDs whose retry delay has passed.
867	async fn on_sweep(&mut self) {
868		let abandoned_user_requests: Vec<UserRequestId> = self
869			.user_requests
870			.iter()
871			.filter_map(|(id, user_request)| user_request.sink.is_closed().then_some(id))
872			.collect();
873		self.metrics
874			.record_outbound(outbound_events::ABANDONED, abandoned_user_requests.len());
875		for id in abandoned_user_requests {
876			log::trace!(target: LOG_TARGET, "dropping abandoned user request {id:?}");
877			self.drop_user_request(id);
878		}
879
880		let now = Instant::now();
881		let (mut cids_to_top_up, timed_out_count) = self.scheduler.expire_peer_timeouts(now);
882		self.metrics.record_outbound(outbound_events::TIMED_OUT, timed_out_count);
883		let restarted_cids = self.scheduler.restart_exhausted_rounds(now);
884		self.metrics
885			.record_outbound(outbound_events::ROUND_RESTARTED, restarted_cids.len());
886		cids_to_top_up.extend(restarted_cids);
887		self.top_up_in_flight(cids_to_top_up).await;
888
889		// Recover capacity after a lookup task exits without a result.
890		self.dispatch_inbound_lookups();
891	}
892
893	/// Queues an inbound peer wantlist and records entries dropped by backpressure.
894	/// Available lookup workers are dispatched immediately after admission.
895	fn on_inbound_request(&mut self, peer: litep2p::PeerId, cids: Vec<(Cid, WantType)>) {
896		let dropped = self.inbound_queue.enqueue(peer, cids);
897		if dropped > 0 {
898			self.metrics.record_inbound_queue_overflow(dropped);
899			log::debug!(
900				target: LOG_TARGET,
901				"inbound queue for {peer:?} full; dropped {dropped} wantlist entries",
902			);
903		}
904		self.dispatch_inbound_lookups();
905	}
906
907	/// Starts queued inbound lookups while worker permits remain available.
908	/// Work stays queued when either the pool is full or no batch is ready.
909	fn dispatch_inbound_lookups(&mut self) {
910		while let Some(permit) = self.inbound_lookup_pool.try_acquire_worker() {
911			let Some((peer, batch)) = self.inbound_queue.next_batch() else { break };
912			self.inbound_lookup_pool.submit(permit, peer, batch);
913		}
914	}
915
916	fn update_metrics(&self) {
917		self.metrics.set_state(
918			self.scheduler.live_cids,
919			self.scheduler.queued_cids,
920			self.user_requests.len(),
921		);
922	}
923
924	/// Notifies every user request that the service closed and clears scheduler state.
925	/// Send failures are ignored because the corresponding receiver already vanished.
926	fn shutdown_user_requests(&mut self) {
927		for (_, user_request) in self.user_requests.drain() {
928			let _ = user_request.sink.try_send(Err(BitswapError::ServiceClosed));
929		}
930		self.scheduler.clear();
931		self.update_metrics();
932	}
933}
934
935/// Resolves an inbound wantlist against locally indexed transactions.
936/// Unsupported CIDs and missing transactions become `DONT_HAVE` responses.
937fn serve_inbound<B: BlockT>(
938	client: &(dyn BlockBackend<B> + Send + Sync),
939	cids: Vec<(Cid, WantType)>,
940	metrics: &BitswapMetrics,
941) -> Vec<ResponseType> {
942	let started = std::time::Instant::now();
943	let mut responses = Vec::with_capacity(cids.len());
944	for (cid, want_type) in cids {
945		let response = {
946			if !is_cid_supported(&cid) {
947				metrics.record_entry(metric_outcomes::UNSUPPORTED_CID);
948				responses
949					.push(ResponseType::Presence { cid, presence: BlockPresenceType::DontHave });
950				continue;
951			}
952			// Supported CIDs always carry a 32-byte digest.
953			let hash = H256::from_slice(&cid.hash().digest()[0..32]);
954			match want_type {
955				// A HAVE query only needs an existence check: `has_indexed_transaction`
956				// answers it via `Database::contains` without loading the transaction body.
957				WantType::Have => match client.has_indexed_transaction(hash) {
958					Ok(true) => ResponseType::Presence { cid, presence: BlockPresenceType::Have },
959					Ok(false) => {
960						ResponseType::Presence { cid, presence: BlockPresenceType::DontHave }
961					},
962					Err(e) => {
963						metrics.record_error(metric_errors::CLIENT);
964						log::error!(
965							target: LOG_TARGET,
966							"has_indexed_transaction({hash}) failed: {e}",
967						);
968						ResponseType::Presence { cid, presence: BlockPresenceType::DontHave }
969					},
970				},
971				WantType::Block => match client.indexed_transaction(hash) {
972					Ok(Some(transaction)) => ResponseType::Block { cid, block: transaction },
973					Ok(None) => {
974						ResponseType::Presence { cid, presence: BlockPresenceType::DontHave }
975					},
976					Err(e) => {
977						metrics.record_error(metric_errors::CLIENT);
978						log::error!(target: LOG_TARGET, "indexed_transaction({hash}) failed: {e}");
979						ResponseType::Presence { cid, presence: BlockPresenceType::DontHave }
980					},
981				},
982			}
983		};
984		metrics.record_response(&response);
985		responses.push(response);
986	}
987	metrics.record_response_bytes(&responses);
988	metrics.record_duration(started.elapsed());
989	responses
990}
991
992#[cfg(test)]
993mod tests;