referrerpolicy=no-referrer-when-downgrade

sc_network_sync/
block_request_handler.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//! Helper for handling (i.e. answering) block requests from a remote peer via the
19//! `crate::request_responses::RequestResponsesBehaviour`.
20
21use crate::{
22	block_relay_protocol::{BlockDownloader, BlockRelayParams, BlockResponseError, BlockServer},
23	schema::v1::{
24		block_request::FromBlock as FromBlockSchema, BlockRequest as BlockRequestSchema,
25		BlockResponse as BlockResponseSchema, BlockResponse, Direction,
26	},
27	service::network::NetworkServiceHandle,
28	LOG_TARGET,
29};
30
31use codec::{Decode, DecodeAll, Encode};
32use futures::{channel::oneshot, stream::StreamExt};
33use log::debug;
34use prost::Message;
35use schnellru::{ByLength, LruMap};
36
37use sc_client_api::BlockBackend;
38use sc_network::{
39	config::ProtocolId,
40	request_responses::{IfDisconnected, IncomingRequest, OutgoingResponse, RequestFailure},
41	service::traits::RequestResponseConfig,
42	types::ProtocolName,
43	NetworkBackend, MAX_RESPONSE_SIZE,
44};
45use sc_network_common::sync::message::{BlockAttributes, BlockData, BlockRequest, FromBlock};
46use sc_network_types::PeerId;
47use sp_blockchain::HeaderBackend;
48use sp_runtime::{
49	generic::BlockId,
50	traits::{Block as BlockT, Header, One, Zero},
51};
52
53use std::{
54	cmp::min,
55	hash::{Hash, Hasher},
56	sync::Arc,
57	time::{Duration, Instant},
58};
59
60/// Maximum blocks per response.
61pub(crate) const MAX_BLOCKS_IN_RESPONSE: usize = 128;
62
63const MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER: usize = 2;
64
65/// Reset duplicate counts this long after the first fulfilled response, allowing legitimate
66/// retries.
67const SAME_REQUEST_WINDOW: Duration = Duration::from_secs(60);
68
69mod rep {
70	use sc_network::ReputationChange as Rep;
71
72	/// Reputation change when a peer sent us the same request multiple times.
73	pub const SAME_REQUEST: Rep = Rep::new(-(1 << 12), "Same block request multiple times");
74
75	/// Reputation change when a peer sent us the same "small" request multiple times.
76	pub const SAME_SMALL_REQUEST: Rep =
77		Rep::new(-(1 << 10), "same small block request multiple times");
78}
79
80/// Generates a `RequestResponseProtocolConfig` for the block request protocol,
81/// refusing incoming requests.
82pub fn generate_protocol_config<
83	Hash: AsRef<[u8]>,
84	B: BlockT,
85	N: NetworkBackend<B, <B as BlockT>::Hash>,
86>(
87	protocol_id: &ProtocolId,
88	genesis_hash: Hash,
89	fork_id: Option<&str>,
90	inbound_queue: async_channel::Sender<IncomingRequest>,
91) -> N::RequestResponseProtocolConfig {
92	N::request_response_config(
93		generate_protocol_name(genesis_hash, fork_id).into(),
94		std::iter::once(generate_legacy_protocol_name(protocol_id).into()).collect(),
95		1024 * 1024,
96		MAX_RESPONSE_SIZE,
97		Duration::from_secs(20),
98		Some(inbound_queue),
99	)
100}
101
102/// Generate the block protocol name from the genesis hash and fork id.
103fn generate_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> String {
104	let genesis_hash = genesis_hash.as_ref();
105	if let Some(fork_id) = fork_id {
106		format!("/{}/{}/sync/2", array_bytes::bytes2hex("", genesis_hash), fork_id)
107	} else {
108		format!("/{}/sync/2", array_bytes::bytes2hex("", genesis_hash))
109	}
110}
111
112/// Generate the legacy block protocol name from chain specific protocol identifier.
113fn generate_legacy_protocol_name(protocol_id: &ProtocolId) -> String {
114	format!("/{}/sync/2", protocol_id.as_ref())
115}
116
117/// The key of [`BlockRequestHandler::seen_requests`].
118#[derive(Eq, PartialEq, Clone)]
119struct SeenRequestsKey<B: BlockT> {
120	peer: PeerId,
121	from: BlockId<B>,
122	max_blocks: usize,
123	direction: Direction,
124	attributes: BlockAttributes,
125	support_multiple_justifications: bool,
126}
127
128#[allow(clippy::derived_hash_with_manual_eq)]
129impl<B: BlockT> Hash for SeenRequestsKey<B> {
130	fn hash<H: Hasher>(&self, state: &mut H) {
131		self.peer.hash(state);
132		self.max_blocks.hash(state);
133		self.direction.hash(state);
134		self.attributes.hash(state);
135		self.support_multiple_justifications.hash(state);
136		match self.from {
137			BlockId::Hash(h) => h.hash(state),
138			BlockId::Number(n) => n.hash(state),
139		}
140	}
141}
142
143/// The value of [`BlockRequestHandler::seen_requests`].
144enum SeenRequestsValue {
145	/// First time we have seen the request.
146	First,
147	/// Requests seen since the first fulfilled response.
148	Fulfilled { requests: usize, since: Instant },
149}
150
151/// The full block server implementation of [`BlockServer`]. It handles
152/// the incoming block requests from a remote peer.
153pub struct BlockRequestHandler<B: BlockT, Client> {
154	client: Arc<Client>,
155	request_receiver: async_channel::Receiver<IncomingRequest>,
156	/// Maps from request to number of times we have seen this request.
157	///
158	/// This is used to check if a peer is spamming us with the same request.
159	seen_requests: LruMap<SeenRequestsKey<B>, SeenRequestsValue>,
160}
161
162impl<B, Client> BlockRequestHandler<B, Client>
163where
164	B: BlockT,
165	Client: HeaderBackend<B> + BlockBackend<B> + Send + Sync + 'static,
166{
167	/// Create a new [`BlockRequestHandler`].
168	pub fn new<N: NetworkBackend<B, <B as BlockT>::Hash>>(
169		network: NetworkServiceHandle,
170		protocol_id: &ProtocolId,
171		fork_id: Option<&str>,
172		client: Arc<Client>,
173		num_peer_hint: usize,
174	) -> BlockRelayParams<B, N> {
175		// Reserve enough request slots for one request per peer when we are at the maximum
176		// number of peers.
177		let capacity = std::cmp::max(num_peer_hint, 1);
178		let (tx, request_receiver) = async_channel::bounded(capacity);
179
180		let protocol_config = generate_protocol_config::<_, B, N>(
181			protocol_id,
182			client
183				.block_hash(0u32.into())
184				.ok()
185				.flatten()
186				.expect("Genesis block exists; qed"),
187			fork_id,
188			tx,
189		);
190
191		let capacity = ByLength::new(num_peer_hint.max(1) as u32 * 2);
192		let seen_requests = LruMap::new(capacity);
193
194		BlockRelayParams {
195			server: Box::new(Self { client, request_receiver, seen_requests }),
196			downloader: Arc::new(FullBlockDownloader::new(
197				protocol_config.protocol_name().clone(),
198				network,
199			)),
200			request_response_config: protocol_config,
201		}
202	}
203
204	/// Run [`BlockRequestHandler`].
205	async fn process_requests(&mut self) {
206		while let Some(request) = self.request_receiver.next().await {
207			let IncomingRequest { peer, payload, pending_response } = request;
208
209			match self.handle_request(payload, pending_response, &peer) {
210				Ok(()) => debug!(target: LOG_TARGET, "Handled block request from {}.", peer),
211				Err(e) => debug!(
212					target: LOG_TARGET,
213					"Failed to handle block request from {}: {}", peer, e,
214				),
215			}
216		}
217	}
218
219	fn handle_request(
220		&mut self,
221		payload: Vec<u8>,
222		pending_response: oneshot::Sender<OutgoingResponse>,
223		peer: &PeerId,
224	) -> Result<(), HandleRequestError> {
225		let request = crate::schema::v1::BlockRequest::decode(&payload[..])?;
226
227		let from_block_id = match request.from_block.ok_or(HandleRequestError::MissingFromField)? {
228			FromBlockSchema::Hash(ref h) => {
229				let h = Decode::decode(&mut h.as_ref())?;
230				BlockId::<B>::Hash(h)
231			},
232			FromBlockSchema::Number(ref n) => {
233				let n = Decode::decode(&mut n.as_ref())?;
234				BlockId::<B>::Number(n)
235			},
236		};
237
238		let max_blocks = if request.max_blocks == 0 {
239			MAX_BLOCKS_IN_RESPONSE
240		} else {
241			min(request.max_blocks as usize, MAX_BLOCKS_IN_RESPONSE)
242		};
243
244		let direction =
245			i32::try_into(request.direction).map_err(|_| HandleRequestError::ParseDirection)?;
246
247		let attributes = BlockAttributes::from_be_u32(request.fields)?;
248
249		let support_multiple_justifications = request.support_multiple_justifications;
250
251		let key = SeenRequestsKey {
252			peer: *peer,
253			max_blocks,
254			direction,
255			from: from_block_id,
256			attributes,
257			support_multiple_justifications,
258		};
259
260		let mut reputation_change = None;
261
262		let small_request = attributes
263			.difference(BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION)
264			.is_empty();
265
266		match self.seen_requests.get(&key) {
267			Some(SeenRequestsValue::First) => {},
268			Some(SeenRequestsValue::Fulfilled { requests, since })
269				if since.elapsed() <= SAME_REQUEST_WINDOW =>
270			{
271				*requests = requests.saturating_add(1);
272
273				if *requests > MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER {
274					reputation_change = Some(if small_request {
275						rep::SAME_SMALL_REQUEST
276					} else {
277						rep::SAME_REQUEST
278					});
279				}
280			},
281			Some(value @ SeenRequestsValue::Fulfilled { .. }) => {
282				*value = SeenRequestsValue::First;
283			},
284			None => {
285				self.seen_requests.insert(key.clone(), SeenRequestsValue::First);
286			},
287		}
288
289		debug!(
290			target: LOG_TARGET,
291			"Handling block request from {peer}: Starting at `{from_block_id:?}` with \
292			maximum blocks of `{max_blocks}`, reputation_change: `{reputation_change:?}`, \
293			small_request `{small_request:?}`, direction `{direction:?}` and \
294			attributes `{attributes:?}`.",
295		);
296
297		let maybe_block_response = if reputation_change.is_none() || small_request {
298			let block_response = self.get_block_response(
299				attributes,
300				from_block_id,
301				direction,
302				max_blocks,
303				support_multiple_justifications,
304			)?;
305
306			// If any of the blocks contains any data, we can consider it as successful request.
307			if block_response
308				.blocks
309				.iter()
310				.any(|b| !b.header.is_empty() || !b.body.is_empty() || b.is_empty_justification)
311			{
312				if let Some(value) = self.seen_requests.get(&key) {
313					if let SeenRequestsValue::First = value {
314						*value =
315							SeenRequestsValue::Fulfilled { requests: 1, since: Instant::now() };
316					}
317				}
318			}
319
320			Some(block_response)
321		} else {
322			None
323		};
324
325		debug!(
326			target: LOG_TARGET,
327			"Sending result of block request from {peer} starting at `{from_block_id:?}`: \
328			blocks: {:?}, data: {:?}",
329			maybe_block_response.as_ref().map(|res| res.blocks.len()),
330			maybe_block_response.as_ref().map(|res| res.encoded_len()),
331		);
332
333		let result = if let Some(block_response) = maybe_block_response {
334			let mut data = Vec::with_capacity(block_response.encoded_len());
335			block_response.encode(&mut data)?;
336			Ok(data)
337		} else {
338			Err(())
339		};
340
341		pending_response
342			.send(OutgoingResponse {
343				result,
344				reputation_changes: reputation_change.into_iter().collect(),
345				sent_feedback: None,
346			})
347			.map_err(|_| HandleRequestError::SendResponse)
348	}
349
350	fn get_block_response(
351		&self,
352		attributes: BlockAttributes,
353		mut block_id: BlockId<B>,
354		direction: Direction,
355		max_blocks: usize,
356		support_multiple_justifications: bool,
357	) -> Result<BlockResponse, HandleRequestError> {
358		let get_header = attributes.contains(BlockAttributes::HEADER);
359		let get_body = attributes.contains(BlockAttributes::BODY);
360		let get_indexed_body = attributes.contains(BlockAttributes::INDEXED_BODY);
361		let get_justification = attributes.contains(BlockAttributes::JUSTIFICATION);
362
363		let mut blocks = Vec::new();
364
365		let mut total_size: usize = 0;
366
367		let client_header_from_block_id =
368			|block_id: BlockId<B>| -> Result<Option<B::Header>, HandleRequestError> {
369				if let Some(hash) = self.client.block_hash_from_id(&block_id)? {
370					return self.client.header(hash).map_err(Into::into);
371				}
372				Ok(None)
373			};
374
375		while let Some(header) = client_header_from_block_id(block_id).unwrap_or_default() {
376			let number = *header.number();
377			let hash = header.hash();
378			let parent_hash = *header.parent_hash();
379			let justifications =
380				if get_justification { self.client.justifications(hash)? } else { None };
381
382			let (justifications, justification, is_empty_justification) =
383				if support_multiple_justifications {
384					let justifications = match justifications {
385						Some(v) => v.encode(),
386						None => Vec::new(),
387					};
388					(justifications, Vec::new(), false)
389				} else {
390					// For now we keep compatibility by selecting precisely the GRANDPA one, and not
391					// just the first one. When sending we could have just taken the first one,
392					// since we don't expect there to be any other kind currently, but when
393					// receiving we need to add the engine ID tag.
394					// The ID tag is hardcoded here to avoid depending on the GRANDPA crate, and
395					// will be removed once we remove the backwards compatibility.
396					// See: https://github.com/paritytech/substrate/issues/8172
397					let justification =
398						justifications.and_then(|just| just.into_justification(*b"FRNK"));
399
400					let is_empty_justification =
401						justification.as_ref().map(|j| j.is_empty()).unwrap_or(false);
402
403					let justification = justification.unwrap_or_default();
404
405					(Vec::new(), justification, is_empty_justification)
406				};
407
408			let body = if get_body {
409				match self.client.block_body(hash)? {
410					Some(mut extrinsics) => {
411						extrinsics.iter_mut().map(|extrinsic| extrinsic.encode()).collect()
412					},
413					None => {
414						log::trace!(target: LOG_TARGET, "Missing data for block request.");
415						break;
416					},
417				}
418			} else {
419				Vec::new()
420			};
421
422			let indexed_body = if get_indexed_body {
423				match self.client.block_indexed_body(hash)? {
424					Some(transactions) => transactions,
425					None => {
426						log::trace!(
427							target: LOG_TARGET,
428							"Missing indexed block data for block request."
429						);
430						// If the indexed body is missing we still continue returning headers.
431						// Ideally `None` should distinguish a missing body from the empty body,
432						// but the current protobuf based protocol does not allow it.
433						Vec::new()
434					},
435				}
436			} else {
437				Vec::new()
438			};
439
440			let block_data = crate::schema::v1::BlockData {
441				hash: hash.encode(),
442				header: if get_header { header.encode() } else { Vec::new() },
443				body,
444				receipt: Vec::new(),
445				message_queue: Vec::new(),
446				justification,
447				is_empty_justification,
448				justifications,
449				indexed_body,
450			};
451
452			let new_total_size = total_size + block_data.encoded_len();
453
454			// Reserve 20 KiB for protocol overhead (length prefixes of `BlockData` + the final
455			// encoding in `BlockResponse`)
456			if new_total_size > (MAX_RESPONSE_SIZE as usize - 20 * 1024) {
457				if blocks.is_empty() {
458					log::error!(
459						target: LOG_TARGET,
460						"Single block response is bigger than the max allowed response size! This is a bug!"
461					);
462				}
463
464				break;
465			}
466
467			total_size = new_total_size;
468
469			blocks.push(block_data);
470
471			if blocks.len() >= max_blocks as usize {
472				break;
473			}
474
475			match direction {
476				Direction::Ascending => block_id = BlockId::Number(number + One::one()),
477				Direction::Descending => {
478					if number.is_zero() {
479						break;
480					}
481					block_id = BlockId::Hash(parent_hash)
482				},
483			}
484		}
485
486		Ok(BlockResponse { blocks })
487	}
488}
489
490#[async_trait::async_trait]
491impl<B, Client> BlockServer<B> for BlockRequestHandler<B, Client>
492where
493	B: BlockT,
494	Client: HeaderBackend<B> + BlockBackend<B> + Send + Sync + 'static,
495{
496	async fn run(&mut self) {
497		self.process_requests().await;
498	}
499}
500
501#[derive(Debug, thiserror::Error)]
502enum HandleRequestError {
503	#[error("Failed to decode request: {0}.")]
504	DecodeProto(#[from] prost::DecodeError),
505	#[error("Failed to encode response: {0}.")]
506	EncodeProto(#[from] prost::EncodeError),
507	#[error("Failed to decode block hash: {0}.")]
508	DecodeScale(#[from] codec::Error),
509	#[error("Missing `BlockRequest::from_block` field.")]
510	MissingFromField,
511	#[error("Failed to parse BlockRequest::direction.")]
512	ParseDirection,
513	#[error(transparent)]
514	Client(#[from] sp_blockchain::Error),
515	#[error("Failed to send response.")]
516	SendResponse,
517}
518
519/// The full block downloader implementation of [`BlockDownloader].
520#[derive(Debug)]
521pub struct FullBlockDownloader {
522	protocol_name: ProtocolName,
523	network: NetworkServiceHandle,
524}
525
526impl FullBlockDownloader {
527	fn new(protocol_name: ProtocolName, network: NetworkServiceHandle) -> Self {
528		Self { protocol_name, network }
529	}
530
531	/// Extracts the blocks from the response schema.
532	fn blocks_from_schema<B: BlockT>(
533		&self,
534		request: &BlockRequest<B>,
535		response: BlockResponseSchema,
536	) -> Result<Vec<BlockData<B>>, String> {
537		response
538			.blocks
539			.into_iter()
540			.map(|block_data| {
541				Ok(BlockData::<B> {
542					hash: Decode::decode(&mut block_data.hash.as_ref())?,
543					header: if !block_data.header.is_empty() {
544						Some(Decode::decode(&mut block_data.header.as_ref())?)
545					} else {
546						None
547					},
548					body: if request.fields.contains(BlockAttributes::BODY) {
549						Some(
550							block_data
551								.body
552								.iter()
553								.map(|body| Decode::decode(&mut body.as_ref()))
554								.collect::<Result<Vec<_>, _>>()?,
555						)
556					} else {
557						None
558					},
559					indexed_body: if request.fields.contains(BlockAttributes::INDEXED_BODY) {
560						Some(block_data.indexed_body)
561					} else {
562						None
563					},
564					receipt: if !block_data.receipt.is_empty() {
565						Some(block_data.receipt)
566					} else {
567						None
568					},
569					message_queue: if !block_data.message_queue.is_empty() {
570						Some(block_data.message_queue)
571					} else {
572						None
573					},
574					justification: if !block_data.justification.is_empty() {
575						Some(block_data.justification)
576					} else if block_data.is_empty_justification {
577						Some(Vec::new())
578					} else {
579						None
580					},
581					justifications: if !block_data.justifications.is_empty() {
582						Some(DecodeAll::decode_all(&mut block_data.justifications.as_ref())?)
583					} else {
584						None
585					},
586				})
587			})
588			.collect::<Result<_, _>>()
589			.map_err(|error: codec::Error| error.to_string())
590	}
591}
592
593#[async_trait::async_trait]
594impl<B: BlockT> BlockDownloader<B> for FullBlockDownloader {
595	fn protocol_name(&self) -> &ProtocolName {
596		&self.protocol_name
597	}
598
599	async fn download_blocks(
600		&self,
601		who: PeerId,
602		request: BlockRequest<B>,
603	) -> Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled> {
604		// Build the request protobuf.
605		let bytes = BlockRequestSchema {
606			fields: request.fields.to_be_u32(),
607			from_block: match request.from {
608				FromBlock::Hash(h) => Some(FromBlockSchema::Hash(h.encode())),
609				FromBlock::Number(n) => Some(FromBlockSchema::Number(n.encode())),
610			},
611			direction: request.direction as i32,
612			max_blocks: request.max.unwrap_or(0),
613			support_multiple_justifications: true,
614		}
615		.encode_to_vec();
616
617		let (tx, rx) = oneshot::channel();
618		self.network.start_request(
619			who,
620			self.protocol_name.clone(),
621			bytes,
622			tx,
623			IfDisconnected::ImmediateError,
624		);
625		rx.await
626	}
627
628	fn block_response_into_blocks(
629		&self,
630		request: &BlockRequest<B>,
631		response: Vec<u8>,
632	) -> Result<Vec<BlockData<B>>, BlockResponseError> {
633		// Decode the response protobuf
634		let response_schema = BlockResponseSchema::decode(response.as_slice())
635			.map_err(|error| BlockResponseError::DecodeFailed(error.to_string()))?;
636
637		// Extract the block data from the protobuf
638		self.blocks_from_schema::<B>(request, response_schema)
639			.map_err(|error| BlockResponseError::ExtractionFailed(error.to_string()))
640	}
641}
642
643#[cfg(test)]
644mod tests {
645	use super::*;
646	use futures::executor::block_on;
647	use sc_block_builder::BlockBuilderBuilder;
648	use sp_consensus::BlockOrigin;
649	use substrate_test_runtime_client::{
650		runtime::Block, ClientBlockImportExt, DefaultTestClientBuilderExt, TestClient,
651		TestClientBuilder, TestClientBuilderExt,
652	};
653
654	fn test_handler() -> BlockRequestHandler<Block, TestClient> {
655		let client = Arc::new(TestClientBuilder::new().build());
656
657		let block = BlockBuilderBuilder::new(&*client)
658			.on_parent_block(client.chain_info().genesis_hash)
659			.with_parent_block_number(0)
660			.build()
661			.unwrap()
662			.build()
663			.unwrap()
664			.block;
665		block_on(client.import(BlockOrigin::Own, block)).unwrap();
666
667		let (_tx, request_receiver) = async_channel::bounded(1);
668		BlockRequestHandler {
669			client,
670			request_receiver,
671			seen_requests: LruMap::new(ByLength::new(16)),
672		}
673	}
674
675	fn make_request(attributes: BlockAttributes) -> Vec<u8> {
676		BlockRequestSchema {
677			fields: attributes.to_be_u32(),
678			from_block: Some(FromBlockSchema::Number(Encode::encode(&1u64))),
679			direction: Direction::Ascending as i32,
680			max_blocks: 1,
681			support_multiple_justifications: true,
682		}
683		.encode_to_vec()
684	}
685
686	fn send_request(
687		handler: &mut BlockRequestHandler<Block, TestClient>,
688		peer: &PeerId,
689		attributes: BlockAttributes,
690	) -> OutgoingResponse {
691		let (tx, mut rx) = oneshot::channel();
692		handler.handle_request(make_request(attributes), tx, peer).unwrap();
693		rx.try_recv().unwrap().unwrap()
694	}
695
696	#[test]
697	fn same_request_limit_resets_after_window() {
698		let mut handler = test_handler();
699		let peer = PeerId::random();
700		let attributes = BlockAttributes::HEADER | BlockAttributes::BODY;
701
702		for _ in 0..MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER {
703			let response = send_request(&mut handler, &peer, attributes);
704			assert!(response.result.is_ok());
705			assert!(response.reputation_changes.is_empty());
706		}
707
708		let response = send_request(&mut handler, &peer, attributes);
709		assert!(response.result.is_err());
710		assert_eq!(response.reputation_changes, vec![rep::SAME_REQUEST]);
711		assert!(rep::SAME_REQUEST.value > i32::MIN);
712
713		// Expire the window without sleeping.
714		let key = SeenRequestsKey::<Block> {
715			peer,
716			from: BlockId::Number(1),
717			max_blocks: 1,
718			direction: Direction::Ascending,
719			attributes,
720			support_multiple_justifications: true,
721		};
722		match handler.seen_requests.get(&key) {
723			Some(SeenRequestsValue::Fulfilled { since, .. }) => {
724				*since = Instant::now() - SAME_REQUEST_WINDOW - Duration::from_secs(1)
725			},
726			_ => panic!("entry must be in the fulfilled state"),
727		}
728
729		let response = send_request(&mut handler, &peer, attributes);
730		assert!(response.result.is_ok());
731		assert!(response.reputation_changes.is_empty());
732
733		// The new window has the same limit.
734		let response = send_request(&mut handler, &peer, attributes);
735		assert!(response.result.is_ok());
736		let response = send_request(&mut handler, &peer, attributes);
737		assert!(response.result.is_err());
738		assert_eq!(response.reputation_changes, vec![rep::SAME_REQUEST]);
739	}
740
741	#[test]
742	fn same_small_request_answered_but_penalized() {
743		let mut handler = test_handler();
744		let peer = PeerId::random();
745		// Header-only requests remain answerable after the limit.
746		let attributes = BlockAttributes::HEADER;
747
748		for _ in 0..MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER {
749			let response = send_request(&mut handler, &peer, attributes);
750			assert!(response.result.is_ok());
751			assert!(response.reputation_changes.is_empty());
752		}
753
754		let response = send_request(&mut handler, &peer, attributes);
755		assert!(response.result.is_ok());
756		assert_eq!(response.reputation_changes, vec![rep::SAME_SMALL_REQUEST]);
757	}
758}