reth_transaction_pool/
config.rs

1use crate::{
2    maintain::MAX_QUEUED_TRANSACTION_LIFETIME,
3    pool::{NEW_TX_LISTENER_BUFFER_SIZE, PENDING_TX_LISTENER_BUFFER_SIZE},
4    PoolSize, TransactionOrigin,
5};
6use alloy_consensus::constants::EIP4844_TX_TYPE_ID;
7use alloy_eips::eip1559::{ETHEREUM_BLOCK_GAS_LIMIT_30M, MIN_PROTOCOL_BASE_FEE};
8use alloy_primitives::Address;
9use std::{collections::HashSet, ops::Mul, time::Duration};
10
11/// Guarantees max transactions for one sender, compatible with geth/erigon
12pub const TXPOOL_MAX_ACCOUNT_SLOTS_PER_SENDER: usize = 16;
13
14/// The default maximum allowed number of transactions in the given subpool.
15pub const TXPOOL_SUBPOOL_MAX_TXS_DEFAULT: usize = 10_000;
16
17/// The default maximum allowed size of the given subpool.
18pub const TXPOOL_SUBPOOL_MAX_SIZE_MB_DEFAULT: usize = 20;
19
20/// The default additional validation tasks size.
21pub const DEFAULT_TXPOOL_ADDITIONAL_VALIDATION_TASKS: usize = 1;
22
23/// Default price bump (in %) for the transaction pool underpriced check.
24pub const DEFAULT_PRICE_BUMP: u128 = 10;
25
26/// Replace blob price bump (in %) for the transaction pool underpriced check.
27///
28/// This enforces that a blob transaction requires a 100% price bump to be replaced
29pub const REPLACE_BLOB_PRICE_BUMP: u128 = 100;
30
31/// Default maximum new transactions for broadcasting.
32pub const MAX_NEW_PENDING_TXS_NOTIFICATIONS: usize = 200;
33
34/// Configuration options for the Transaction pool.
35#[derive(Debug, Clone)]
36pub struct PoolConfig {
37    /// Max number of transaction in the pending sub-pool
38    pub pending_limit: SubPoolLimit,
39    /// Max number of transaction in the basefee sub-pool
40    pub basefee_limit: SubPoolLimit,
41    /// Max number of transaction in the queued sub-pool
42    pub queued_limit: SubPoolLimit,
43    /// Max number of transactions in the blob sub-pool
44    pub blob_limit: SubPoolLimit,
45    /// Max number of executable transaction slots guaranteed per account
46    pub max_account_slots: usize,
47    /// Price bump (in %) for the transaction pool underpriced check.
48    pub price_bumps: PriceBumpConfig,
49    /// Minimum base fee required by the protocol.
50    pub minimal_protocol_basefee: u64,
51    /// The max gas limit for transactions in the pool
52    pub gas_limit: u64,
53    /// How to handle locally received transactions:
54    /// [`TransactionOrigin::Local`](TransactionOrigin).
55    pub local_transactions_config: LocalTransactionConfig,
56    /// Bound on number of pending transactions from `reth_network::TransactionsManager` to buffer.
57    pub pending_tx_listener_buffer_size: usize,
58    /// Bound on number of new transactions from `reth_network::TransactionsManager` to buffer.
59    pub new_tx_listener_buffer_size: usize,
60    /// How many new pending transactions to buffer and send iterators in progress.
61    pub max_new_pending_txs_notifications: usize,
62    /// Maximum lifetime for transactions in the pool
63    pub max_queued_lifetime: Duration,
64}
65
66impl PoolConfig {
67    /// Returns whether the size and amount constraints in any sub-pools are exceeded.
68    #[inline]
69    pub const fn is_exceeded(&self, pool_size: PoolSize) -> bool {
70        self.blob_limit.is_exceeded(pool_size.blob, pool_size.blob_size) ||
71            self.pending_limit.is_exceeded(pool_size.pending, pool_size.pending_size) ||
72            self.basefee_limit.is_exceeded(pool_size.basefee, pool_size.basefee_size) ||
73            self.queued_limit.is_exceeded(pool_size.queued, pool_size.queued_size)
74    }
75}
76
77impl Default for PoolConfig {
78    fn default() -> Self {
79        Self {
80            pending_limit: Default::default(),
81            basefee_limit: Default::default(),
82            queued_limit: Default::default(),
83            blob_limit: Default::default(),
84            max_account_slots: TXPOOL_MAX_ACCOUNT_SLOTS_PER_SENDER,
85            price_bumps: Default::default(),
86            minimal_protocol_basefee: MIN_PROTOCOL_BASE_FEE,
87            gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
88            local_transactions_config: Default::default(),
89            pending_tx_listener_buffer_size: PENDING_TX_LISTENER_BUFFER_SIZE,
90            new_tx_listener_buffer_size: NEW_TX_LISTENER_BUFFER_SIZE,
91            max_new_pending_txs_notifications: MAX_NEW_PENDING_TXS_NOTIFICATIONS,
92            max_queued_lifetime: MAX_QUEUED_TRANSACTION_LIFETIME,
93        }
94    }
95}
96
97/// Size limits for a sub-pool.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct SubPoolLimit {
100    /// Maximum amount of transaction in the pool.
101    pub max_txs: usize,
102    /// Maximum combined size (in bytes) of transactions in the pool.
103    pub max_size: usize,
104}
105
106impl SubPoolLimit {
107    /// Creates a new instance with the given limits.
108    pub const fn new(max_txs: usize, max_size: usize) -> Self {
109        Self { max_txs, max_size }
110    }
111
112    /// Returns whether the size or amount constraint is violated.
113    #[inline]
114    pub const fn is_exceeded(&self, txs: usize, size: usize) -> bool {
115        self.max_txs < txs || self.max_size < size
116    }
117}
118
119impl Mul<usize> for SubPoolLimit {
120    type Output = Self;
121
122    fn mul(self, rhs: usize) -> Self::Output {
123        let Self { max_txs, max_size } = self;
124        Self { max_txs: max_txs * rhs, max_size: max_size * rhs }
125    }
126}
127
128impl Default for SubPoolLimit {
129    fn default() -> Self {
130        // either 10k transactions or 20MB
131        Self {
132            max_txs: TXPOOL_SUBPOOL_MAX_TXS_DEFAULT,
133            max_size: TXPOOL_SUBPOOL_MAX_SIZE_MB_DEFAULT * 1024 * 1024,
134        }
135    }
136}
137
138/// Price bump config (in %) for the transaction pool underpriced check.
139#[derive(Debug, Clone, Copy, Eq, PartialEq)]
140pub struct PriceBumpConfig {
141    /// Default price bump (in %) for the transaction pool underpriced check.
142    pub default_price_bump: u128,
143    /// Replace blob price bump (in %) for the transaction pool underpriced check.
144    pub replace_blob_tx_price_bump: u128,
145}
146
147impl PriceBumpConfig {
148    /// Returns the price bump required to replace the given transaction type.
149    #[inline]
150    pub(crate) const fn price_bump(&self, tx_type: u8) -> u128 {
151        if tx_type == EIP4844_TX_TYPE_ID {
152            return self.replace_blob_tx_price_bump
153        }
154        self.default_price_bump
155    }
156}
157
158impl Default for PriceBumpConfig {
159    fn default() -> Self {
160        Self {
161            default_price_bump: DEFAULT_PRICE_BUMP,
162            replace_blob_tx_price_bump: REPLACE_BLOB_PRICE_BUMP,
163        }
164    }
165}
166
167/// Configuration options for the locally received transactions:
168/// [`TransactionOrigin::Local`](TransactionOrigin)
169#[derive(Debug, Clone, Eq, PartialEq)]
170pub struct LocalTransactionConfig {
171    /// Apply no exemptions to the locally received transactions.
172    ///
173    /// This includes:
174    ///   - available slots are limited to the configured `max_account_slots` of [`PoolConfig`]
175    ///   - no price exemptions
176    ///   - no eviction exemptions
177    pub no_exemptions: bool,
178    /// Addresses that will be considered as local. Above exemptions apply.
179    pub local_addresses: HashSet<Address>,
180    /// Flag indicating whether local transactions should be propagated.
181    pub propagate_local_transactions: bool,
182}
183
184impl Default for LocalTransactionConfig {
185    fn default() -> Self {
186        Self {
187            no_exemptions: false,
188            local_addresses: HashSet::default(),
189            propagate_local_transactions: true,
190        }
191    }
192}
193
194impl LocalTransactionConfig {
195    /// Returns whether local transactions are not exempt from the configured limits.
196    #[inline]
197    pub const fn no_local_exemptions(&self) -> bool {
198        self.no_exemptions
199    }
200
201    /// Returns whether the local addresses vector contains the given address.
202    #[inline]
203    pub fn contains_local_address(&self, address: &Address) -> bool {
204        self.local_addresses.contains(address)
205    }
206
207    /// Returns whether the particular transaction should be considered local.
208    ///
209    /// This always returns false if the local exemptions are disabled.
210    #[inline]
211    pub fn is_local(&self, origin: TransactionOrigin, sender: &Address) -> bool {
212        if self.no_local_exemptions() {
213            return false
214        }
215        origin.is_local() || self.contains_local_address(sender)
216    }
217
218    /// Sets toggle to propagate transactions received locally by this client (e.g
219    /// transactions from `eth_sendTransaction` to this nodes' RPC server)
220    ///
221    /// If set to false, only transactions received by network peers (via
222    /// p2p) will be marked as propagated in the local transaction pool and returned on a
223    /// `GetPooledTransactions` p2p request
224    pub const fn set_propagate_local_transactions(mut self, propagate_local_txs: bool) -> Self {
225        self.propagate_local_transactions = propagate_local_txs;
226        self
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_pool_size_sanity() {
236        let pool_size = PoolSize {
237            pending: 0,
238            pending_size: 0,
239            basefee: 0,
240            basefee_size: 0,
241            queued: 0,
242            queued_size: 0,
243            blob: 0,
244            blob_size: 0,
245            ..Default::default()
246        };
247
248        // the current size is zero so this should not exceed any limits
249        let config = PoolConfig::default();
250        assert!(!config.is_exceeded(pool_size));
251
252        // set them to be above the limits
253        let pool_size = PoolSize {
254            pending: config.pending_limit.max_txs + 1,
255            pending_size: config.pending_limit.max_size + 1,
256            basefee: config.basefee_limit.max_txs + 1,
257            basefee_size: config.basefee_limit.max_size + 1,
258            queued: config.queued_limit.max_txs + 1,
259            queued_size: config.queued_limit.max_size + 1,
260            blob: config.blob_limit.max_txs + 1,
261            blob_size: config.blob_limit.max_size + 1,
262            ..Default::default()
263        };
264
265        // now this should be above the limits
266        assert!(config.is_exceeded(pool_size));
267    }
268
269    #[test]
270    fn test_default_config() {
271        let config = LocalTransactionConfig::default();
272
273        assert!(!config.no_exemptions);
274        assert!(config.local_addresses.is_empty());
275        assert!(config.propagate_local_transactions);
276    }
277
278    #[test]
279    fn test_no_local_exemptions() {
280        let config = LocalTransactionConfig { no_exemptions: true, ..Default::default() };
281        assert!(config.no_local_exemptions());
282    }
283
284    #[test]
285    fn test_contains_local_address() {
286        let address = Address::new([1; 20]);
287        let mut local_addresses = HashSet::default();
288        local_addresses.insert(address);
289
290        let config = LocalTransactionConfig { local_addresses, ..Default::default() };
291
292        // Should contain the inserted address
293        assert!(config.contains_local_address(&address));
294
295        // Should not contain another random address
296        assert!(!config.contains_local_address(&Address::new([2; 20])));
297    }
298
299    #[test]
300    fn test_is_local_with_no_exemptions() {
301        let address = Address::new([1; 20]);
302        let config = LocalTransactionConfig {
303            no_exemptions: true,
304            local_addresses: HashSet::default(),
305            ..Default::default()
306        };
307
308        // Should return false as no exemptions is set to true
309        assert!(!config.is_local(TransactionOrigin::Local, &address));
310    }
311
312    #[test]
313    fn test_is_local_without_no_exemptions() {
314        let address = Address::new([1; 20]);
315        let mut local_addresses = HashSet::default();
316        local_addresses.insert(address);
317
318        let config =
319            LocalTransactionConfig { no_exemptions: false, local_addresses, ..Default::default() };
320
321        // Should return true as the transaction origin is local
322        assert!(config.is_local(TransactionOrigin::Local, &Address::new([2; 20])));
323        assert!(config.is_local(TransactionOrigin::Local, &address));
324
325        // Should return true as the address is in the local_addresses set
326        assert!(config.is_local(TransactionOrigin::External, &address));
327        // Should return false as the address is not in the local_addresses set
328        assert!(!config.is_local(TransactionOrigin::External, &Address::new([2; 20])));
329    }
330
331    #[test]
332    fn test_set_propagate_local_transactions() {
333        let config = LocalTransactionConfig::default();
334        assert!(config.propagate_local_transactions);
335
336        let new_config = config.set_propagate_local_transactions(false);
337        assert!(!new_config.propagate_local_transactions);
338    }
339
340    #[test]
341    fn scale_pool_limit() {
342        let limit = SubPoolLimit::default();
343        let double = limit * 2;
344        assert_eq!(
345            double,
346            SubPoolLimit { max_txs: limit.max_txs * 2, max_size: limit.max_size * 2 }
347        )
348    }
349}