referrerpolicy=no-referrer-when-downgrade

sc_transaction_pool/graph/
rotator.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//! Rotate extrinsic inside the pool.
20//!
21//! Keeps only recent extrinsic and discard the ones kept for a significant amount of time.
22//! Discarded extrinsics are banned so that they don't get re-imported again.
23
24use parking_lot::RwLock;
25use std::{
26	collections::HashMap,
27	hash, iter,
28	time::{Duration, Instant},
29};
30
31use super::base_pool::Transaction;
32
33/// Expected size of the banned extrinsics cache.
34const DEFAULT_EXPECTED_SIZE: usize = 2048;
35
36/// The default duration, in seconds, for which an extrinsic is banned.
37const DEFAULT_BAN_TIME_SECS: u64 = 30 * 60;
38
39/// Pool rotator is responsible to only keep fresh extrinsics in the pool.
40///
41/// Extrinsics that occupy the pool for too long are culled and temporarily banned from entering
42/// the pool again.
43pub struct PoolRotator<Hash> {
44	/// How long the extrinsic is banned for.
45	ban_time: Duration,
46	/// Currently banned extrinsics.
47	banned_until: RwLock<HashMap<Hash, Instant>>,
48	/// Expected size of the banned extrinsics cache.
49	expected_size: usize,
50}
51
52impl<Hash: Clone> Clone for PoolRotator<Hash> {
53	fn clone(&self) -> Self {
54		Self {
55			ban_time: self.ban_time,
56			banned_until: RwLock::new(self.banned_until.read().clone()),
57			expected_size: self.expected_size,
58		}
59	}
60}
61
62impl<Hash: hash::Hash + Eq> Default for PoolRotator<Hash> {
63	fn default() -> Self {
64		Self {
65			ban_time: Duration::from_secs(DEFAULT_BAN_TIME_SECS),
66			banned_until: Default::default(),
67			expected_size: DEFAULT_EXPECTED_SIZE,
68		}
69	}
70}
71
72impl<Hash: hash::Hash + Eq + Clone> PoolRotator<Hash> {
73	/// New rotator instance with specified ban time.
74	pub fn new(ban_time: Duration) -> Self {
75		Self { ban_time, ..Self::default() }
76	}
77
78	/// New rotator instance with specified ban time and expected cache size.
79	pub fn new_with_expected_size(ban_time: Duration, expected_size: usize) -> Self {
80		Self { expected_size, ..Self::new(ban_time) }
81	}
82
83	/// Returns `true` if extrinsic hash is currently banned.
84	pub fn is_banned(&self, hash: &Hash) -> bool {
85		self.banned_until.read().contains_key(hash)
86	}
87
88	/// Bans given set of hashes.
89	pub fn ban(&self, now: &Instant, hashes: impl IntoIterator<Item = Hash>) {
90		let mut banned = self.banned_until.write();
91
92		for hash in hashes {
93			banned.insert(hash, *now + self.ban_time);
94		}
95
96		if banned.len() > 2 * self.expected_size {
97			while banned.len() > self.expected_size {
98				if let Some(key) = banned.keys().next().cloned() {
99					banned.remove(&key);
100				}
101			}
102		}
103	}
104
105	/// Bans extrinsic if it's stale.
106	///
107	/// Returns `true` if extrinsic is stale and got banned.
108	pub fn ban_if_stale<Ex>(
109		&self,
110		now: &Instant,
111		current_block: u64,
112		xt: &Transaction<Hash, Ex>,
113	) -> bool {
114		if xt.valid_till > current_block {
115			return false
116		}
117
118		self.ban(now, iter::once(xt.hash.clone()));
119		true
120	}
121
122	/// Removes timed bans.
123	pub fn clear_timeouts(&self, now: &Instant) {
124		let mut banned = self.banned_until.write();
125
126		banned.retain(|_, &mut v| v >= *now);
127	}
128}
129
130#[cfg(test)]
131mod tests {
132	use super::*;
133
134	type Hash = u64;
135	type Ex = ();
136
137	fn rotator() -> PoolRotator<Hash> {
138		PoolRotator { ban_time: Duration::from_millis(10), ..Default::default() }
139	}
140
141	fn tx() -> (Hash, Transaction<Hash, Ex>) {
142		let hash = 5u64;
143		let tx = Transaction {
144			data: (),
145			bytes: 1,
146			hash,
147			priority: 5,
148			valid_till: 1,
149			requires: vec![],
150			provides: vec![],
151			propagate: true,
152			source: crate::TimedTransactionSource::new_external(false),
153		};
154
155		(hash, tx)
156	}
157
158	#[test]
159	fn should_not_ban_if_not_stale() {
160		// given
161		let (hash, tx) = tx();
162		let rotator = rotator();
163		assert!(!rotator.is_banned(&hash));
164		let now = Instant::now();
165		let past_block = 0;
166
167		// when
168		assert!(!rotator.ban_if_stale(&now, past_block, &tx));
169
170		// then
171		assert!(!rotator.is_banned(&hash));
172	}
173
174	#[test]
175	fn should_ban_stale_extrinsic() {
176		// given
177		let (hash, tx) = tx();
178		let rotator = rotator();
179		assert!(!rotator.is_banned(&hash));
180
181		// when
182		assert!(rotator.ban_if_stale(&Instant::now(), 1, &tx));
183
184		// then
185		assert!(rotator.is_banned(&hash));
186	}
187
188	#[test]
189	fn should_clear_banned() {
190		// given
191		let (hash, tx) = tx();
192		let rotator = rotator();
193		assert!(rotator.ban_if_stale(&Instant::now(), 1, &tx));
194		assert!(rotator.is_banned(&hash));
195
196		// when
197		let future = Instant::now() + rotator.ban_time + rotator.ban_time;
198		rotator.clear_timeouts(&future);
199
200		// then
201		assert!(!rotator.is_banned(&hash));
202	}
203
204	#[test]
205	fn should_garbage_collect() {
206		// given
207		fn tx_with(i: u64, valid_till: u64) -> Transaction<Hash, Ex> {
208			let hash = i;
209			Transaction {
210				data: (),
211				bytes: 2,
212				hash,
213				priority: 5,
214				valid_till,
215				requires: vec![],
216				provides: vec![],
217				propagate: true,
218				source: crate::TimedTransactionSource::new_external(false),
219			}
220		}
221
222		let rotator = rotator();
223
224		let now = Instant::now();
225		let past_block = 0;
226
227		// when
228		for i in 0..2 * DEFAULT_EXPECTED_SIZE {
229			let tx = tx_with(i as u64, past_block);
230			assert!(rotator.ban_if_stale(&now, past_block, &tx));
231		}
232		assert_eq!(rotator.banned_until.read().len(), 2 * DEFAULT_EXPECTED_SIZE);
233
234		// then
235		let tx = tx_with(2 * DEFAULT_EXPECTED_SIZE as u64, past_block);
236		// trigger a garbage collection
237		assert!(rotator.ban_if_stale(&now, past_block, &tx));
238		assert_eq!(rotator.banned_until.read().len(), DEFAULT_EXPECTED_SIZE);
239	}
240}