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
11pub const TXPOOL_MAX_ACCOUNT_SLOTS_PER_SENDER: usize = 16;
13
14pub const TXPOOL_SUBPOOL_MAX_TXS_DEFAULT: usize = 10_000;
16
17pub const TXPOOL_SUBPOOL_MAX_SIZE_MB_DEFAULT: usize = 20;
19
20pub const DEFAULT_TXPOOL_ADDITIONAL_VALIDATION_TASKS: usize = 1;
22
23pub const DEFAULT_PRICE_BUMP: u128 = 10;
25
26pub const REPLACE_BLOB_PRICE_BUMP: u128 = 100;
30
31pub const MAX_NEW_PENDING_TXS_NOTIFICATIONS: usize = 200;
33
34pub const DEFAULT_MAX_INFLIGHT_DELEGATED_SLOTS: usize = 1;
36
37#[derive(Debug, Clone)]
39pub struct PoolConfig {
40 pub pending_limit: SubPoolLimit,
42 pub basefee_limit: SubPoolLimit,
44 pub queued_limit: SubPoolLimit,
46 pub blob_limit: SubPoolLimit,
48 pub blob_cache_size: Option<u32>,
50 pub max_account_slots: usize,
52 pub price_bumps: PriceBumpConfig,
54 pub minimal_protocol_basefee: u64,
56 pub minimum_priority_fee: Option<u128>,
58 pub gas_limit: u64,
60 pub local_transactions_config: LocalTransactionConfig,
63 pub pending_tx_listener_buffer_size: usize,
65 pub new_tx_listener_buffer_size: usize,
67 pub max_new_pending_txs_notifications: usize,
69 pub max_queued_lifetime: Duration,
71 pub max_inflight_delegated_slot_limit: usize,
75 pub enforce_tracked_nonce: bool,
82}
83
84impl PoolConfig {
85 pub const fn with_disabled_protocol_base_fee(self) -> Self {
89 self.with_protocol_base_fee(0)
90 }
91
92 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 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 pub const fn with_enforce_tracked_nonce(mut self, enforce: bool) -> Self {
113 self.enforce_tracked_nonce = enforce;
114 self
115 }
116
117 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct SubPoolLimit {
154 pub max_txs: usize,
156 pub max_size: usize,
158}
159
160impl SubPoolLimit {
161 pub const fn new(max_txs: usize, max_size: usize) -> Self {
163 Self { max_txs, max_size }
164 }
165
166 pub const fn max() -> Self {
168 Self::new(usize::MAX, usize::MAX)
169 }
170
171 #[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 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 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#[derive(Debug, Clone, Copy, Eq, PartialEq)]
204pub struct PriceBumpConfig {
205 pub default_price_bump: u128,
207 pub replace_blob_tx_price_bump: u128,
209}
210
211impl PriceBumpConfig {
212 #[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 #[inline]
228 pub fn is_replacement_underpriced<T: Transaction + ?Sized>(
229 &self,
230 existing: &T,
231 maybe_replacement: &T,
232 ) -> bool {
233 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 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 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 if let Some(existing_max_blob_fee_per_gas) = existing.max_fee_per_blob_gas() {
261 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#[derive(Debug, Clone, Eq, PartialEq)]
286pub struct LocalTransactionConfig {
287 pub no_exemptions: bool,
294 pub local_addresses: AddressSet,
296 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 #[inline]
313 pub const fn no_local_exemptions(&self) -> bool {
314 self.no_exemptions
315 }
316
317 #[inline]
319 pub fn contains_local_address(&self, address: &Address) -> bool {
320 self.local_addresses.contains(address)
321 }
322
323 #[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 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 let config = PoolConfig::default();
415 assert!(!config.is_exceeded(pool_size));
416
417 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 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 assert!(config.contains_local_address(&address));
459
460 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 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 assert!(config.is_local(TransactionOrigin::Local, &Address::new([2; 20])));
488 assert!(config.is_local(TransactionOrigin::Local, &address));
489
490 assert!(config.is_local(TransactionOrigin::External, &address));
492 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}