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