Skip to main content

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, Transaction};
7use alloy_eips::eip1559::{ETHEREUM_BLOCK_GAS_LIMIT_30M, MIN_PROTOCOL_BASE_FEE};
8use alloy_primitives::{map::AddressSet, Address};
9use std::{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/// Default maximum allowed in flight delegated transactions per account.
35pub const DEFAULT_MAX_INFLIGHT_DELEGATED_SLOTS: usize = 1;
36
37/// Configuration options for the Transaction pool.
38#[derive(Debug, Clone)]
39pub struct PoolConfig {
40    /// Max number of transactions in the pending sub-pool
41    pub pending_limit: SubPoolLimit,
42    /// Max number of transactions in the basefee sub-pool
43    pub basefee_limit: SubPoolLimit,
44    /// Max number of transactions in the queued sub-pool
45    pub queued_limit: SubPoolLimit,
46    /// Max number of transactions in the blob sub-pool
47    pub blob_limit: SubPoolLimit,
48    /// Blob cache size
49    pub blob_cache_size: Option<u32>,
50    /// Max number of executable transaction slots guaranteed per account
51    pub max_account_slots: usize,
52    /// Price bump (in %) for the transaction pool underpriced check.
53    pub price_bumps: PriceBumpConfig,
54    /// Minimum base fee required by the protocol.
55    pub minimal_protocol_basefee: u64,
56    /// Minimum priority fee required for transaction acceptance into the pool.
57    pub minimum_priority_fee: Option<u128>,
58    /// The max gas limit for transactions in the pool
59    pub gas_limit: u64,
60    /// How to handle locally received transactions:
61    /// [`TransactionOrigin::Local`](TransactionOrigin).
62    pub local_transactions_config: LocalTransactionConfig,
63    /// Bound on number of pending transactions from `reth_network::TransactionsManager` to buffer.
64    pub pending_tx_listener_buffer_size: usize,
65    /// Bound on number of new transactions from `reth_network::TransactionsManager` to buffer.
66    pub new_tx_listener_buffer_size: usize,
67    /// How many new pending transactions to buffer and send iterators in progress.
68    pub max_new_pending_txs_notifications: usize,
69    /// Maximum lifetime for transactions in the pool
70    pub max_queued_lifetime: Duration,
71    /// The maximum allowed inflight transactions a delegated sender can have.
72    ///
73    /// This restricts how many executable transaction a delegated sender can stack.
74    pub max_inflight_delegated_slot_limit: usize,
75    /// Whether to enforce the sender nonce tracked from canonical updates over the nonce a
76    /// transaction was validated against, rejecting transactions below it.
77    ///
78    /// Closes the window in which a validation result that predates a block inserts an already
79    /// mined nonce as pending. Assumes sender nonces only move forward, so this is primarily
80    /// recommended for chains without reorgs and very low block times. Disabled by default.
81    pub enforce_tracked_nonce: bool,
82}
83
84impl PoolConfig {
85    /// Sets the minimal protocol base fee to 0, effectively disabling checks that enforce that a
86    /// transaction's fee must be higher than the [`MIN_PROTOCOL_BASE_FEE`] which is the lowest
87    /// value the ethereum EIP-1559 base fee can reach.
88    pub const fn with_disabled_protocol_base_fee(self) -> Self {
89        self.with_protocol_base_fee(0)
90    }
91
92    /// Configures the minimal protocol base fee that should be enforced.
93    ///
94    /// Ethereum's EIP-1559 base fee can't drop below [`MIN_PROTOCOL_BASE_FEE`] hence this is
95    /// enforced by default in the pool.
96    pub const fn with_protocol_base_fee(mut self, protocol_base_fee: u64) -> Self {
97        self.minimal_protocol_basefee = protocol_base_fee;
98        self
99    }
100
101    /// Configures how many slots are available for a delegated sender.
102    pub const fn with_max_inflight_delegated_slots(
103        mut self,
104        max_inflight_delegation_limit: usize,
105    ) -> Self {
106        self.max_inflight_delegated_slot_limit = max_inflight_delegation_limit;
107        self
108    }
109
110    /// Configures whether the sender nonce tracked from canonical updates is enforced on
111    /// insertion, see [`Self::enforce_tracked_nonce`].
112    pub const fn with_enforce_tracked_nonce(mut self, enforce: bool) -> Self {
113        self.enforce_tracked_nonce = enforce;
114        self
115    }
116
117    /// Returns whether the size and amount constraints in any sub-pools are exceeded.
118    #[inline]
119    pub const fn is_exceeded(&self, pool_size: PoolSize) -> bool {
120        self.blob_limit.is_exceeded(pool_size.blob, pool_size.blob_size) ||
121            self.pending_limit.is_exceeded(pool_size.pending, pool_size.pending_size) ||
122            self.basefee_limit.is_exceeded(pool_size.basefee, pool_size.basefee_size) ||
123            self.queued_limit.is_exceeded(pool_size.queued, pool_size.queued_size)
124    }
125}
126
127impl Default for PoolConfig {
128    fn default() -> Self {
129        Self {
130            pending_limit: Default::default(),
131            basefee_limit: Default::default(),
132            queued_limit: Default::default(),
133            blob_limit: Default::default(),
134            blob_cache_size: None,
135            max_account_slots: TXPOOL_MAX_ACCOUNT_SLOTS_PER_SENDER,
136            price_bumps: Default::default(),
137            minimal_protocol_basefee: MIN_PROTOCOL_BASE_FEE,
138            minimum_priority_fee: None,
139            gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
140            local_transactions_config: Default::default(),
141            pending_tx_listener_buffer_size: PENDING_TX_LISTENER_BUFFER_SIZE,
142            new_tx_listener_buffer_size: NEW_TX_LISTENER_BUFFER_SIZE,
143            max_new_pending_txs_notifications: MAX_NEW_PENDING_TXS_NOTIFICATIONS,
144            max_queued_lifetime: MAX_QUEUED_TRANSACTION_LIFETIME,
145            max_inflight_delegated_slot_limit: DEFAULT_MAX_INFLIGHT_DELEGATED_SLOTS,
146            enforce_tracked_nonce: false,
147        }
148    }
149}
150
151/// Size limits for a sub-pool.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct SubPoolLimit {
154    /// Maximum amount of transaction in the pool.
155    pub max_txs: usize,
156    /// Maximum combined size (in bytes) of transactions in the pool.
157    pub max_size: usize,
158}
159
160impl SubPoolLimit {
161    /// Creates a new instance with the given limits.
162    pub const fn new(max_txs: usize, max_size: usize) -> Self {
163        Self { max_txs, max_size }
164    }
165
166    /// Creates an unlimited [`SubPoolLimit`]
167    pub const fn max() -> Self {
168        Self::new(usize::MAX, usize::MAX)
169    }
170
171    /// Returns whether the size or amount constraint is violated.
172    #[inline]
173    pub const fn is_exceeded(&self, txs: usize, size: usize) -> bool {
174        self.max_txs < txs || self.max_size < size
175    }
176
177    /// Returns how many transactions exceed the configured limit.
178    pub const fn tx_excess(&self, txs: usize) -> Option<usize> {
179        txs.checked_sub(self.max_txs)
180    }
181}
182
183impl Mul<usize> for SubPoolLimit {
184    type Output = Self;
185
186    fn mul(self, rhs: usize) -> Self::Output {
187        let Self { max_txs, max_size } = self;
188        Self { max_txs: max_txs * rhs, max_size: max_size * rhs }
189    }
190}
191
192impl Default for SubPoolLimit {
193    fn default() -> Self {
194        // either 10k transactions or 20MB
195        Self {
196            max_txs: TXPOOL_SUBPOOL_MAX_TXS_DEFAULT,
197            max_size: TXPOOL_SUBPOOL_MAX_SIZE_MB_DEFAULT * 1024 * 1024,
198        }
199    }
200}
201
202/// Price bump config (in %) for the transaction pool underpriced check.
203#[derive(Debug, Clone, Copy, Eq, PartialEq)]
204pub struct PriceBumpConfig {
205    /// Default price bump (in %) for the transaction pool underpriced check.
206    pub default_price_bump: u128,
207    /// Replace blob price bump (in %) for the transaction pool underpriced check.
208    pub replace_blob_tx_price_bump: u128,
209}
210
211impl PriceBumpConfig {
212    /// Returns the price bump required to replace the given transaction type.
213    #[inline]
214    pub const fn price_bump(&self, tx_type: u8) -> u128 {
215        if tx_type == EIP4844_TX_TYPE_ID {
216            return self.replace_blob_tx_price_bump
217        }
218        self.default_price_bump
219    }
220
221    /// Determines whether a candidate transaction (`maybe_replacement`) is underpriced compared to
222    /// an existing transaction in the pool.
223    ///
224    /// A transaction is considered underpriced if it doesn't meet the required fee bump threshold.
225    /// This applies to both standard gas fees and, for blob-carrying transactions (EIP-4844),
226    /// the blob-specific fees.
227    #[inline]
228    pub fn is_replacement_underpriced<T: Transaction + ?Sized>(
229        &self,
230        existing: &T,
231        maybe_replacement: &T,
232    ) -> bool {
233        // Retrieve the required price bump percentage for this type of transaction.
234        //
235        // The bump is different for EIP-4844 and other transactions. See `PriceBumpConfig`.
236        let price_bump = self.price_bump(existing.ty());
237        let required_bumped_fee =
238            |existing_fee: u128| existing_fee.saturating_mul(100 + price_bump).div_ceil(100);
239
240        // Check if the max fee per gas is underpriced.
241        if maybe_replacement.max_fee_per_gas() < required_bumped_fee(existing.max_fee_per_gas()) {
242            return true
243        }
244
245        let existing_max_priority_fee_per_gas =
246            existing.max_priority_fee_per_gas().unwrap_or_default();
247        let replacement_max_priority_fee_per_gas =
248            maybe_replacement.max_priority_fee_per_gas().unwrap_or_default();
249
250        // Check max priority fee per gas (relevant for EIP-1559 transactions only)
251        if existing_max_priority_fee_per_gas != 0 &&
252            replacement_max_priority_fee_per_gas != 0 &&
253            replacement_max_priority_fee_per_gas <
254                required_bumped_fee(existing_max_priority_fee_per_gas)
255        {
256            return true
257        }
258
259        // Check max blob fee per gas
260        if let Some(existing_max_blob_fee_per_gas) = existing.max_fee_per_blob_gas() {
261            // This enforces that blob txs can only be replaced by blob txs
262            let replacement_max_blob_fee_per_gas =
263                maybe_replacement.max_fee_per_blob_gas().unwrap_or_default();
264            if replacement_max_blob_fee_per_gas < required_bumped_fee(existing_max_blob_fee_per_gas)
265            {
266                return true
267            }
268        }
269
270        false
271    }
272}
273
274impl Default for PriceBumpConfig {
275    fn default() -> Self {
276        Self {
277            default_price_bump: DEFAULT_PRICE_BUMP,
278            replace_blob_tx_price_bump: REPLACE_BLOB_PRICE_BUMP,
279        }
280    }
281}
282
283/// Configuration options for the locally received transactions:
284/// [`TransactionOrigin::Local`](TransactionOrigin)
285#[derive(Debug, Clone, Eq, PartialEq)]
286pub struct LocalTransactionConfig {
287    /// Apply no exemptions to the locally received transactions.
288    ///
289    /// This includes:
290    ///   - available slots are limited to the configured `max_account_slots` of [`PoolConfig`]
291    ///   - no price exemptions
292    ///   - no eviction exemptions
293    pub no_exemptions: bool,
294    /// Addresses that will be considered as local. Above exemptions apply.
295    pub local_addresses: AddressSet,
296    /// Flag indicating whether local transactions should be propagated.
297    pub propagate_local_transactions: bool,
298}
299
300impl Default for LocalTransactionConfig {
301    fn default() -> Self {
302        Self {
303            no_exemptions: false,
304            local_addresses: AddressSet::default(),
305            propagate_local_transactions: true,
306        }
307    }
308}
309
310impl LocalTransactionConfig {
311    /// Returns whether local transactions are not exempt from the configured limits.
312    #[inline]
313    pub const fn no_local_exemptions(&self) -> bool {
314        self.no_exemptions
315    }
316
317    /// Returns whether the local addresses vector contains the given address.
318    #[inline]
319    pub fn contains_local_address(&self, address: &Address) -> bool {
320        self.local_addresses.contains(address)
321    }
322
323    /// Returns whether the particular transaction should be considered local.
324    ///
325    /// This always returns false if the local exemptions are disabled.
326    #[inline]
327    pub fn is_local(&self, origin: TransactionOrigin, sender: &Address) -> bool {
328        if self.no_local_exemptions() {
329            return false
330        }
331        origin.is_local() || self.contains_local_address(sender)
332    }
333
334    /// Sets toggle to propagate transactions received locally by this client (e.g
335    /// transactions from `eth_sendTransaction` to this nodes' RPC server)
336    ///
337    /// If set to false, only transactions received by network peers (via
338    /// p2p) will be marked as propagated in the local transaction pool and returned on a
339    /// `GetPooledTransactions` p2p request
340    pub const fn set_propagate_local_transactions(mut self, propagate_local_txs: bool) -> Self {
341        self.propagate_local_transactions = propagate_local_txs;
342        self
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use alloy_consensus::{TxEip1559, TxEip4844};
349
350    use super::*;
351
352    #[test]
353    fn replacement_uses_configured_price_bump() {
354        let config = PriceBumpConfig { default_price_bump: 25, ..Default::default() };
355        let existing =
356            TxEip1559 { max_fee_per_gas: 100, max_priority_fee_per_gas: 10, ..Default::default() };
357        let mut replacement = existing.clone();
358        replacement.max_fee_per_gas = 125;
359        replacement.max_priority_fee_per_gas = 12;
360        assert!(config.is_replacement_underpriced(&existing, &replacement));
361
362        replacement.max_priority_fee_per_gas = 13;
363        assert!(!config.is_replacement_underpriced(&existing, &replacement));
364
365        replacement.max_fee_per_gas = 124;
366        assert!(config.is_replacement_underpriced(&existing, &replacement));
367    }
368
369    #[test]
370    fn blob_replacement_requires_all_fee_bumps() {
371        let config = PriceBumpConfig::default();
372        let existing = TxEip4844 {
373            max_fee_per_gas: 100,
374            max_priority_fee_per_gas: 10,
375            max_fee_per_blob_gas: 50,
376            ..Default::default()
377        };
378        let replacement = TxEip4844 {
379            max_fee_per_gas: 200,
380            max_priority_fee_per_gas: 20,
381            max_fee_per_blob_gas: 100,
382            ..existing.clone()
383        };
384        assert!(!config.is_replacement_underpriced(&existing, &replacement));
385
386        let mut underpriced = replacement.clone();
387        underpriced.max_fee_per_gas -= 1;
388        assert!(config.is_replacement_underpriced(&existing, &underpriced));
389
390        let mut underpriced = replacement.clone();
391        underpriced.max_priority_fee_per_gas -= 1;
392        assert!(config.is_replacement_underpriced(&existing, &underpriced));
393
394        let mut underpriced = replacement;
395        underpriced.max_fee_per_blob_gas -= 1;
396        assert!(config.is_replacement_underpriced(&existing, &underpriced));
397    }
398
399    #[test]
400    fn test_pool_size_sanity() {
401        let pool_size = PoolSize {
402            pending: 0,
403            pending_size: 0,
404            basefee: 0,
405            basefee_size: 0,
406            queued: 0,
407            queued_size: 0,
408            blob: 0,
409            blob_size: 0,
410            ..Default::default()
411        };
412
413        // the current size is zero so this should not exceed any limits
414        let config = PoolConfig::default();
415        assert!(!config.is_exceeded(pool_size));
416
417        // set them to be above the limits
418        let pool_size = PoolSize {
419            pending: config.pending_limit.max_txs + 1,
420            pending_size: config.pending_limit.max_size + 1,
421            basefee: config.basefee_limit.max_txs + 1,
422            basefee_size: config.basefee_limit.max_size + 1,
423            queued: config.queued_limit.max_txs + 1,
424            queued_size: config.queued_limit.max_size + 1,
425            blob: config.blob_limit.max_txs + 1,
426            blob_size: config.blob_limit.max_size + 1,
427            ..Default::default()
428        };
429
430        // now this should be above the limits
431        assert!(config.is_exceeded(pool_size));
432    }
433
434    #[test]
435    fn test_default_config() {
436        let config = LocalTransactionConfig::default();
437
438        assert!(!config.no_exemptions);
439        assert!(config.local_addresses.is_empty());
440        assert!(config.propagate_local_transactions);
441    }
442
443    #[test]
444    fn test_no_local_exemptions() {
445        let config = LocalTransactionConfig { no_exemptions: true, ..Default::default() };
446        assert!(config.no_local_exemptions());
447    }
448
449    #[test]
450    fn test_contains_local_address() {
451        let address = Address::new([1; 20]);
452        let mut local_addresses = AddressSet::default();
453        local_addresses.insert(address);
454
455        let config = LocalTransactionConfig { local_addresses, ..Default::default() };
456
457        // Should contain the inserted address
458        assert!(config.contains_local_address(&address));
459
460        // Should not contain another random address
461        assert!(!config.contains_local_address(&Address::new([2; 20])));
462    }
463
464    #[test]
465    fn test_is_local_with_no_exemptions() {
466        let address = Address::new([1; 20]);
467        let config = LocalTransactionConfig {
468            no_exemptions: true,
469            local_addresses: AddressSet::default(),
470            ..Default::default()
471        };
472
473        // Should return false as no exemptions is set to true
474        assert!(!config.is_local(TransactionOrigin::Local, &address));
475    }
476
477    #[test]
478    fn test_is_local_without_no_exemptions() {
479        let address = Address::new([1; 20]);
480        let mut local_addresses = AddressSet::default();
481        local_addresses.insert(address);
482
483        let config =
484            LocalTransactionConfig { no_exemptions: false, local_addresses, ..Default::default() };
485
486        // Should return true as the transaction origin is local
487        assert!(config.is_local(TransactionOrigin::Local, &Address::new([2; 20])));
488        assert!(config.is_local(TransactionOrigin::Local, &address));
489
490        // Should return true as the address is in the local_addresses set
491        assert!(config.is_local(TransactionOrigin::External, &address));
492        // Should return false as the address is not in the local_addresses set
493        assert!(!config.is_local(TransactionOrigin::External, &Address::new([2; 20])));
494    }
495
496    #[test]
497    fn test_set_propagate_local_transactions() {
498        let config = LocalTransactionConfig::default();
499        assert!(config.propagate_local_transactions);
500
501        let new_config = config.set_propagate_local_transactions(false);
502        assert!(!new_config.propagate_local_transactions);
503    }
504
505    #[test]
506    fn scale_pool_limit() {
507        let limit = SubPoolLimit::default();
508        let double = limit * 2;
509        assert_eq!(
510            double,
511            SubPoolLimit { max_txs: limit.max_txs * 2, max_size: limit.max_size * 2 }
512        )
513    }
514}