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/// 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 transaction in the pending sub-pool
41    pub pending_limit: SubPoolLimit,
42    /// Max number of transaction in the basefee sub-pool
43    pub basefee_limit: SubPoolLimit,
44    /// Max number of transaction 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}
76
77impl PoolConfig {
78    /// Sets the minimal protocol base fee to 0, effectively disabling checks that enforce that a
79    /// transaction's fee must be higher than the [`MIN_PROTOCOL_BASE_FEE`] which is the lowest
80    /// value the ethereum EIP-1559 base fee can reach.
81    pub const fn with_disabled_protocol_base_fee(self) -> Self {
82        self.with_protocol_base_fee(0)
83    }
84
85    /// Configures the minimal protocol base fee that should be enforced.
86    ///
87    /// Ethereum's EIP-1559 base fee can't drop below [`MIN_PROTOCOL_BASE_FEE`] hence this is
88    /// enforced by default in the pool.
89    pub const fn with_protocol_base_fee(mut self, protocol_base_fee: u64) -> Self {
90        self.minimal_protocol_basefee = protocol_base_fee;
91        self
92    }
93
94    /// Configures how many slots are available for a delegated sender.
95    pub const fn with_max_inflight_delegated_slots(
96        mut self,
97        max_inflight_delegation_limit: usize,
98    ) -> Self {
99        self.max_inflight_delegated_slot_limit = max_inflight_delegation_limit;
100        self
101    }
102
103    /// Returns whether the size and amount constraints in any sub-pools are exceeded.
104    #[inline]
105    pub const fn is_exceeded(&self, pool_size: PoolSize) -> bool {
106        self.blob_limit.is_exceeded(pool_size.blob, pool_size.blob_size) ||
107            self.pending_limit.is_exceeded(pool_size.pending, pool_size.pending_size) ||
108            self.basefee_limit.is_exceeded(pool_size.basefee, pool_size.basefee_size) ||
109            self.queued_limit.is_exceeded(pool_size.queued, pool_size.queued_size)
110    }
111}
112
113impl Default for PoolConfig {
114    fn default() -> Self {
115        Self {
116            pending_limit: Default::default(),
117            basefee_limit: Default::default(),
118            queued_limit: Default::default(),
119            blob_limit: Default::default(),
120            blob_cache_size: None,
121            max_account_slots: TXPOOL_MAX_ACCOUNT_SLOTS_PER_SENDER,
122            price_bumps: Default::default(),
123            minimal_protocol_basefee: MIN_PROTOCOL_BASE_FEE,
124            minimum_priority_fee: None,
125            gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
126            local_transactions_config: Default::default(),
127            pending_tx_listener_buffer_size: PENDING_TX_LISTENER_BUFFER_SIZE,
128            new_tx_listener_buffer_size: NEW_TX_LISTENER_BUFFER_SIZE,
129            max_new_pending_txs_notifications: MAX_NEW_PENDING_TXS_NOTIFICATIONS,
130            max_queued_lifetime: MAX_QUEUED_TRANSACTION_LIFETIME,
131            max_inflight_delegated_slot_limit: DEFAULT_MAX_INFLIGHT_DELEGATED_SLOTS,
132        }
133    }
134}
135
136/// Size limits for a sub-pool.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct SubPoolLimit {
139    /// Maximum amount of transaction in the pool.
140    pub max_txs: usize,
141    /// Maximum combined size (in bytes) of transactions in the pool.
142    pub max_size: usize,
143}
144
145impl SubPoolLimit {
146    /// Creates a new instance with the given limits.
147    pub const fn new(max_txs: usize, max_size: usize) -> Self {
148        Self { max_txs, max_size }
149    }
150
151    /// Creates an unlimited [`SubPoolLimit`]
152    pub const fn max() -> Self {
153        Self::new(usize::MAX, usize::MAX)
154    }
155
156    /// Returns whether the size or amount constraint is violated.
157    #[inline]
158    pub const fn is_exceeded(&self, txs: usize, size: usize) -> bool {
159        self.max_txs < txs || self.max_size < size
160    }
161
162    /// Returns how many transactions exceed the configured limit.
163    pub const fn tx_excess(&self, txs: usize) -> Option<usize> {
164        txs.checked_sub(self.max_txs)
165    }
166}
167
168impl Mul<usize> for SubPoolLimit {
169    type Output = Self;
170
171    fn mul(self, rhs: usize) -> Self::Output {
172        let Self { max_txs, max_size } = self;
173        Self { max_txs: max_txs * rhs, max_size: max_size * rhs }
174    }
175}
176
177impl Default for SubPoolLimit {
178    fn default() -> Self {
179        // either 10k transactions or 20MB
180        Self {
181            max_txs: TXPOOL_SUBPOOL_MAX_TXS_DEFAULT,
182            max_size: TXPOOL_SUBPOOL_MAX_SIZE_MB_DEFAULT * 1024 * 1024,
183        }
184    }
185}
186
187/// Price bump config (in %) for the transaction pool underpriced check.
188#[derive(Debug, Clone, Copy, Eq, PartialEq)]
189pub struct PriceBumpConfig {
190    /// Default price bump (in %) for the transaction pool underpriced check.
191    pub default_price_bump: u128,
192    /// Replace blob price bump (in %) for the transaction pool underpriced check.
193    pub replace_blob_tx_price_bump: u128,
194}
195
196impl PriceBumpConfig {
197    /// Returns the price bump required to replace the given transaction type.
198    #[inline]
199    pub const fn price_bump(&self, tx_type: u8) -> u128 {
200        if tx_type == EIP4844_TX_TYPE_ID {
201            return self.replace_blob_tx_price_bump
202        }
203        self.default_price_bump
204    }
205}
206
207impl Default for PriceBumpConfig {
208    fn default() -> Self {
209        Self {
210            default_price_bump: DEFAULT_PRICE_BUMP,
211            replace_blob_tx_price_bump: REPLACE_BLOB_PRICE_BUMP,
212        }
213    }
214}
215
216/// Configuration options for the locally received transactions:
217/// [`TransactionOrigin::Local`](TransactionOrigin)
218#[derive(Debug, Clone, Eq, PartialEq)]
219pub struct LocalTransactionConfig {
220    /// Apply no exemptions to the locally received transactions.
221    ///
222    /// This includes:
223    ///   - available slots are limited to the configured `max_account_slots` of [`PoolConfig`]
224    ///   - no price exemptions
225    ///   - no eviction exemptions
226    pub no_exemptions: bool,
227    /// Addresses that will be considered as local. Above exemptions apply.
228    pub local_addresses: HashSet<Address>,
229    /// Flag indicating whether local transactions should be propagated.
230    pub propagate_local_transactions: bool,
231}
232
233impl Default for LocalTransactionConfig {
234    fn default() -> Self {
235        Self {
236            no_exemptions: false,
237            local_addresses: HashSet::default(),
238            propagate_local_transactions: true,
239        }
240    }
241}
242
243impl LocalTransactionConfig {
244    /// Returns whether local transactions are not exempt from the configured limits.
245    #[inline]
246    pub const fn no_local_exemptions(&self) -> bool {
247        self.no_exemptions
248    }
249
250    /// Returns whether the local addresses vector contains the given address.
251    #[inline]
252    pub fn contains_local_address(&self, address: &Address) -> bool {
253        self.local_addresses.contains(address)
254    }
255
256    /// Returns whether the particular transaction should be considered local.
257    ///
258    /// This always returns false if the local exemptions are disabled.
259    #[inline]
260    pub fn is_local(&self, origin: TransactionOrigin, sender: &Address) -> bool {
261        if self.no_local_exemptions() {
262            return false
263        }
264        origin.is_local() || self.contains_local_address(sender)
265    }
266
267    /// Sets toggle to propagate transactions received locally by this client (e.g
268    /// transactions from `eth_sendTransaction` to this nodes' RPC server)
269    ///
270    /// If set to false, only transactions received by network peers (via
271    /// p2p) will be marked as propagated in the local transaction pool and returned on a
272    /// `GetPooledTransactions` p2p request
273    pub const fn set_propagate_local_transactions(mut self, propagate_local_txs: bool) -> Self {
274        self.propagate_local_transactions = propagate_local_txs;
275        self
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_pool_size_sanity() {
285        let pool_size = PoolSize {
286            pending: 0,
287            pending_size: 0,
288            basefee: 0,
289            basefee_size: 0,
290            queued: 0,
291            queued_size: 0,
292            blob: 0,
293            blob_size: 0,
294            ..Default::default()
295        };
296
297        // the current size is zero so this should not exceed any limits
298        let config = PoolConfig::default();
299        assert!(!config.is_exceeded(pool_size));
300
301        // set them to be above the limits
302        let pool_size = PoolSize {
303            pending: config.pending_limit.max_txs + 1,
304            pending_size: config.pending_limit.max_size + 1,
305            basefee: config.basefee_limit.max_txs + 1,
306            basefee_size: config.basefee_limit.max_size + 1,
307            queued: config.queued_limit.max_txs + 1,
308            queued_size: config.queued_limit.max_size + 1,
309            blob: config.blob_limit.max_txs + 1,
310            blob_size: config.blob_limit.max_size + 1,
311            ..Default::default()
312        };
313
314        // now this should be above the limits
315        assert!(config.is_exceeded(pool_size));
316    }
317
318    #[test]
319    fn test_default_config() {
320        let config = LocalTransactionConfig::default();
321
322        assert!(!config.no_exemptions);
323        assert!(config.local_addresses.is_empty());
324        assert!(config.propagate_local_transactions);
325    }
326
327    #[test]
328    fn test_no_local_exemptions() {
329        let config = LocalTransactionConfig { no_exemptions: true, ..Default::default() };
330        assert!(config.no_local_exemptions());
331    }
332
333    #[test]
334    fn test_contains_local_address() {
335        let address = Address::new([1; 20]);
336        let mut local_addresses = HashSet::default();
337        local_addresses.insert(address);
338
339        let config = LocalTransactionConfig { local_addresses, ..Default::default() };
340
341        // Should contain the inserted address
342        assert!(config.contains_local_address(&address));
343
344        // Should not contain another random address
345        assert!(!config.contains_local_address(&Address::new([2; 20])));
346    }
347
348    #[test]
349    fn test_is_local_with_no_exemptions() {
350        let address = Address::new([1; 20]);
351        let config = LocalTransactionConfig {
352            no_exemptions: true,
353            local_addresses: HashSet::default(),
354            ..Default::default()
355        };
356
357        // Should return false as no exemptions is set to true
358        assert!(!config.is_local(TransactionOrigin::Local, &address));
359    }
360
361    #[test]
362    fn test_is_local_without_no_exemptions() {
363        let address = Address::new([1; 20]);
364        let mut local_addresses = HashSet::default();
365        local_addresses.insert(address);
366
367        let config =
368            LocalTransactionConfig { no_exemptions: false, local_addresses, ..Default::default() };
369
370        // Should return true as the transaction origin is local
371        assert!(config.is_local(TransactionOrigin::Local, &Address::new([2; 20])));
372        assert!(config.is_local(TransactionOrigin::Local, &address));
373
374        // Should return true as the address is in the local_addresses set
375        assert!(config.is_local(TransactionOrigin::External, &address));
376        // Should return false as the address is not in the local_addresses set
377        assert!(!config.is_local(TransactionOrigin::External, &Address::new([2; 20])));
378    }
379
380    #[test]
381    fn test_set_propagate_local_transactions() {
382        let config = LocalTransactionConfig::default();
383        assert!(config.propagate_local_transactions);
384
385        let new_config = config.set_propagate_local_transactions(false);
386        assert!(!new_config.propagate_local_transactions);
387    }
388
389    #[test]
390    fn scale_pool_limit() {
391        let limit = SubPoolLimit::default();
392        let double = limit * 2;
393        assert_eq!(
394            double,
395            SubPoolLimit { max_txs: limit.max_txs * 2, max_size: limit.max_size * 2 }
396        )
397    }
398}