reth_transaction_pool/error.rs
1//! Transaction pool errors
2
3use std::any::Any;
4
5use alloy_eips::eip4844::BlobTransactionValidationError;
6use alloy_primitives::{Address, TxHash, U256};
7use reth_primitives_traits::transaction::error::InvalidTransactionError;
8
9/// Transaction pool result type.
10pub type PoolResult<T> = Result<T, PoolError>;
11
12/// A trait for additional errors that can be thrown by the transaction pool.
13///
14/// For example during validation
15/// [`TransactionValidator::validate_transaction`](crate::validate::TransactionValidator::validate_transaction)
16pub trait PoolTransactionError: core::error::Error + Send + Sync {
17 /// Returns `true` if the error was caused by a transaction that is considered bad in the
18 /// context of the transaction pool and warrants peer penalization.
19 ///
20 /// See [`PoolError::is_bad_transaction`].
21 fn is_bad_transaction(&self) -> bool;
22
23 /// Returns a reference to `self` as a `&dyn Any`, enabling downcasting.
24 fn as_any(&self) -> &dyn Any;
25}
26
27// Needed for `#[error(transparent)]`
28impl core::error::Error for Box<dyn PoolTransactionError> {
29 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
30 (**self).source()
31 }
32}
33
34/// Transaction pool error.
35#[derive(Debug, thiserror::Error)]
36#[error("[{hash}]: {kind}")]
37pub struct PoolError {
38 /// The transaction hash that caused the error.
39 pub hash: TxHash,
40 /// The error kind.
41 pub kind: PoolErrorKind,
42}
43
44/// Transaction pool error kind.
45#[derive(Debug, thiserror::Error)]
46pub enum PoolErrorKind {
47 /// Same transaction already imported
48 #[error("already imported")]
49 AlreadyImported,
50 /// Thrown if a replacement transaction's gas price is below the already imported transaction
51 #[error("insufficient gas price to replace existing transaction")]
52 ReplacementUnderpriced,
53 /// The fee cap of the transaction is below the minimum fee cap determined by the protocol
54 #[error("transaction feeCap {0} below chain minimum")]
55 FeeCapBelowMinimumProtocolFeeCap(u128),
56 /// Thrown when the number of unique transactions of a sender exceeded the slot capacity.
57 #[error("rejected due to {0} being identified as a spammer")]
58 SpammerExceededCapacity(Address),
59 /// Thrown when a new transaction is added to the pool, but then immediately discarded to
60 /// respect the size limits of the pool.
61 #[error("transaction discarded outright due to pool size constraints")]
62 DiscardedOnInsert,
63 /// Thrown when the transaction is considered invalid.
64 #[error(transparent)]
65 InvalidTransaction(#[from] InvalidPoolTransactionError),
66 /// Thrown if the mutual exclusivity constraint (blob vs normal transaction) is violated.
67 #[error("transaction type {1} conflicts with existing transaction for {0}")]
68 ExistingConflictingTransactionType(Address, u8),
69 /// Any other error that occurred while inserting/validating a transaction. e.g. IO database
70 /// error
71 #[error(transparent)]
72 Other(#[from] Box<dyn core::error::Error + Send + Sync>),
73}
74
75// === impl PoolError ===
76
77impl PoolError {
78 /// Creates a new pool error.
79 pub fn new(hash: TxHash, kind: impl Into<PoolErrorKind>) -> Self {
80 Self { hash, kind: kind.into() }
81 }
82
83 /// Creates a new pool error with the `Other` kind.
84 pub fn other(
85 hash: TxHash,
86 error: impl Into<Box<dyn core::error::Error + Send + Sync>>,
87 ) -> Self {
88 Self { hash, kind: PoolErrorKind::Other(error.into()) }
89 }
90
91 /// Returns `true` if the error was caused by a transaction that is considered bad in the
92 /// context of the transaction pool and warrants peer penalization.
93 ///
94 /// Not all error variants are caused by the incorrect composition of the transaction (See also
95 /// [`InvalidPoolTransactionError`]) and can be caused by the current state of the transaction
96 /// pool. For example the transaction pool is already full or the error was caused by an
97 /// internal error, such as database errors.
98 ///
99 /// This function returns true only if the transaction will never make it into the pool because
100 /// its composition is invalid and the original sender should have detected this as well. This
101 /// is used to determine whether the original sender should be penalized for sending an
102 /// erroneous transaction.
103 #[inline]
104 pub fn is_bad_transaction(&self) -> bool {
105 #[expect(clippy::match_same_arms)]
106 match &self.kind {
107 PoolErrorKind::AlreadyImported => {
108 // already imported but not bad
109 false
110 }
111 PoolErrorKind::ReplacementUnderpriced => {
112 // already imported but not bad
113 false
114 }
115 PoolErrorKind::FeeCapBelowMinimumProtocolFeeCap(_) => {
116 // fee cap of the tx below the technical minimum determined by the protocol, see
117 // [MINIMUM_PROTOCOL_FEE_CAP](alloy_primitives::constants::MIN_PROTOCOL_BASE_FEE)
118 // although this transaction will always be invalid, we do not want to penalize the
119 // sender because this check simply could not be implemented by the client
120 false
121 }
122 PoolErrorKind::SpammerExceededCapacity(_) => {
123 // the sender exceeded the slot capacity, we should not penalize the peer for
124 // sending the tx because we don't know if all the transactions are sent from the
125 // same peer, there's also a chance that old transactions haven't been cleared yet
126 // (pool lags behind) and old transaction still occupy a slot in the pool
127 false
128 }
129 PoolErrorKind::DiscardedOnInsert => {
130 // valid tx but dropped due to size constraints
131 false
132 }
133 PoolErrorKind::InvalidTransaction(err) => {
134 // transaction rejected because it violates constraints
135 err.is_bad_transaction()
136 }
137 PoolErrorKind::Other(_) => {
138 // internal error unrelated to the transaction
139 false
140 }
141 PoolErrorKind::ExistingConflictingTransactionType(_, _) => {
142 // this is not a protocol error but an implementation error since the pool enforces
143 // exclusivity (blob vs normal tx) for all senders
144 false
145 }
146 }
147 }
148
149 /// Returns `true` if this is a blob sidecar error that should NOT be cached as a bad import.
150 ///
151 /// The transaction hash may be valid — the issue is peer-specific (e.g. malformed sidecar
152 /// data), so we penalize the peer but allow re-fetching from other peers.
153 #[inline]
154 pub const fn is_bad_blob_sidecar(&self) -> bool {
155 match &self.kind {
156 PoolErrorKind::InvalidTransaction(err) => err.is_bad_blob_sidecar(),
157 _ => false,
158 }
159 }
160}
161
162/// Represents all errors that can happen when validating transactions for the pool for EIP-4844
163/// transactions
164#[derive(Debug, thiserror::Error)]
165pub enum Eip4844PoolTransactionError {
166 /// Thrown if we're unable to find the blob for a transaction that was previously extracted
167 #[error("blob sidecar not found for EIP4844 transaction")]
168 MissingEip4844BlobSidecar,
169 /// Thrown if an EIP-4844 transaction without any blobs arrives
170 #[error("blobless blob transaction")]
171 NoEip4844Blobs,
172 /// Thrown if an EIP-4844 transaction arrives with too many blobs
173 #[error("too many blobs in transaction: have {have}, permitted {permitted}")]
174 TooManyEip4844Blobs {
175 /// Number of blobs the transaction has
176 have: u64,
177 /// Number of maximum blobs the transaction can have
178 permitted: u64,
179 },
180 /// Thrown if validating the blob sidecar for the transaction failed.
181 #[error(transparent)]
182 InvalidEip4844Blob(BlobTransactionValidationError),
183 /// EIP-4844 transactions are only accepted if they're gapless, meaning the previous nonce of
184 /// the transaction (`tx.nonce -1`) must either be in the pool or match the on chain nonce of
185 /// the sender.
186 ///
187 /// This error is thrown on validation if a valid blob transaction arrives with a nonce that
188 /// would introduce gap in the nonce sequence.
189 #[error("nonce too high")]
190 Eip4844NonceGap,
191 /// Thrown if blob transaction has an EIP-7594 style sidecar before Osaka.
192 #[error("unexpected eip-7594 sidecar before osaka")]
193 UnexpectedEip7594SidecarBeforeOsaka,
194 /// Thrown if blob transaction has an EIP-4844 style sidecar after Osaka.
195 #[error("unexpected eip-4844 sidecar after osaka")]
196 UnexpectedEip4844SidecarAfterOsaka,
197 /// Thrown if blob transaction has an EIP-7594 style sidecar but EIP-7594 support is disabled.
198 #[error("eip-7594 sidecar disallowed")]
199 Eip7594SidecarDisallowed,
200}
201
202/// Represents all errors that can happen when validating transactions for the pool for EIP-7702
203/// transactions
204#[derive(Debug, thiserror::Error)]
205pub enum Eip7702PoolTransactionError {
206 /// Thrown if the transaction has no items in its authorization list
207 #[error("no items in authorization list for EIP7702 transaction")]
208 MissingEip7702AuthorizationList,
209 /// Returned when a transaction with a nonce
210 /// gap is received from accounts with a deployed delegation or pending delegation.
211 #[error("gapped-nonce tx from delegated accounts")]
212 OutOfOrderTxFromDelegated,
213 /// Returned when the maximum number of in-flight
214 /// transactions is reached for specific accounts.
215 #[error("in-flight transaction limit reached for delegated accounts")]
216 InflightTxLimitReached,
217 /// Returned if a transaction has an authorization
218 /// signed by an address which already has in-flight transactions known to the
219 /// pool.
220 #[error("authority already reserved")]
221 AuthorityReserved,
222}
223
224/// Represents errors that can happen when validating transactions for the pool
225///
226/// See [`TransactionValidator`](crate::TransactionValidator).
227#[derive(Debug, thiserror::Error)]
228pub enum InvalidPoolTransactionError {
229 /// Hard consensus errors
230 #[error(transparent)]
231 Consensus(#[from] InvalidTransactionError),
232 /// Thrown when a new transaction is added to the pool, but then immediately discarded to
233 /// respect the size limits of the pool.
234 #[error("transaction's gas limit {0} exceeds block's gas limit {1}")]
235 ExceedsGasLimit(u64, u64),
236 /// Thrown when a transaction's gas limit exceeds the configured maximum per-transaction limit.
237 #[error("transaction's gas limit {0} exceeds maximum per-transaction gas limit {1}")]
238 MaxTxGasLimitExceeded(u64, u64),
239 /// Thrown when a new transaction is added to the pool, but then immediately discarded to
240 /// respect the tx fee exceeds the configured cap
241 #[error("tx fee ({max_tx_fee_wei} wei) exceeds the configured cap ({tx_fee_cap_wei} wei)")]
242 ExceedsFeeCap {
243 /// max fee in wei of new tx submitted to the pool (e.g. 0.11534 ETH)
244 max_tx_fee_wei: u128,
245 /// configured tx fee cap in wei (e.g. 1.0 ETH)
246 tx_fee_cap_wei: u128,
247 },
248 /// Thrown when a new transaction is added to the pool, but then immediately discarded to
249 /// respect the `max_init_code_size`.
250 #[error("transaction's input size {0} exceeds max_init_code_size {1}")]
251 ExceedsMaxInitCodeSize(usize, usize),
252 /// Thrown if the input data of a transaction is greater
253 /// than some meaningful limit a user might use. This is not a consensus error
254 /// making the transaction invalid, rather a DOS protection.
255 #[error("oversized data: transaction size {size}, limit {limit}")]
256 OversizedData {
257 /// Size of the transaction/input data that exceeded the limit.
258 size: usize,
259 /// Configured limit that was exceeded.
260 limit: usize,
261 },
262 /// Thrown if the transaction's fee is below the minimum fee
263 #[error("transaction underpriced")]
264 Underpriced,
265 /// Thrown if the transaction's would require an account to be overdrawn
266 #[error("transaction overdraws from account, balance: {balance}, cost: {cost}")]
267 Overdraft {
268 /// Cost transaction is allowed to consume. See `reth_transaction_pool::PoolTransaction`.
269 cost: U256,
270 /// Balance of account.
271 balance: U256,
272 },
273 /// EIP-2681 error thrown if the nonce is higher or equal than `U64::max`
274 /// `<https://eips.ethereum.org/EIPS/eip-2681>`
275 #[error("nonce exceeds u64 limit")]
276 Eip2681,
277 /// EIP-4844 related errors
278 #[error(transparent)]
279 Eip4844(#[from] Eip4844PoolTransactionError),
280 /// EIP-7702 related errors
281 #[error(transparent)]
282 Eip7702(#[from] Eip7702PoolTransactionError),
283 /// Any other error that occurred while inserting/validating that is transaction specific
284 #[error(transparent)]
285 Other(Box<dyn PoolTransactionError>),
286 /// The transaction is specified to use less gas than required to start the
287 /// invocation.
288 #[error("intrinsic gas too low")]
289 IntrinsicGasTooLow,
290 /// The transaction priority fee is below the minimum required priority fee.
291 #[error("transaction priority fee below minimum required priority fee {minimum_priority_fee}")]
292 PriorityFeeBelowMinimum {
293 /// Minimum required priority fee.
294 minimum_priority_fee: u128,
295 },
296}
297
298// === impl InvalidPoolTransactionError ===
299
300impl InvalidPoolTransactionError {
301 /// Returns a new [`InvalidPoolTransactionError::Other`] instance with the given
302 /// [`PoolTransactionError`].
303 pub fn other<E: PoolTransactionError + 'static>(err: E) -> Self {
304 Self::Other(Box::new(err))
305 }
306
307 /// Returns `true` if the error was caused by a transaction that is considered bad in the
308 /// context of the transaction pool and warrants peer penalization.
309 ///
310 /// See [`PoolError::is_bad_transaction`].
311 #[expect(clippy::match_same_arms)]
312 #[inline]
313 fn is_bad_transaction(&self) -> bool {
314 match self {
315 Self::Consensus(err) => {
316 // transaction considered invalid by the consensus rules
317 // We do not consider the following errors to be erroneous transactions, since they
318 // depend on dynamic environmental conditions and should not be assumed to have been
319 // intentionally caused by the sender
320 match err {
321 InvalidTransactionError::InsufficientFunds { .. } |
322 InvalidTransactionError::NonceNotConsistent { .. } => {
323 // transaction could just have arrived late/early
324 false
325 }
326 InvalidTransactionError::GasTooLow |
327 InvalidTransactionError::GasTooHigh |
328 InvalidTransactionError::TipAboveFeeCap => {
329 // these are technically not invalid
330 false
331 }
332 InvalidTransactionError::FeeCapTooLow => {
333 // dynamic, but not used during validation
334 false
335 }
336 InvalidTransactionError::Eip2930Disabled |
337 InvalidTransactionError::Eip1559Disabled |
338 InvalidTransactionError::Eip4844Disabled |
339 InvalidTransactionError::Eip7702Disabled => {
340 // settings
341 false
342 }
343 InvalidTransactionError::OldLegacyChainId |
344 InvalidTransactionError::ChainIdMismatch |
345 InvalidTransactionError::GasUintOverflow |
346 InvalidTransactionError::TxTypeNotSupported |
347 InvalidTransactionError::SignerAccountHasBytecode |
348 InvalidTransactionError::GasLimitTooHigh => true,
349 }
350 }
351 Self::ExceedsGasLimit(_, _) => true,
352 Self::MaxTxGasLimitExceeded(_, _) => {
353 // local setting
354 false
355 }
356 Self::ExceedsFeeCap { max_tx_fee_wei: _, tx_fee_cap_wei: _ } => true,
357 Self::ExceedsMaxInitCodeSize(_, _) => true,
358 Self::OversizedData { .. } => true,
359 Self::Underpriced => {
360 // local setting
361 false
362 }
363 Self::IntrinsicGasTooLow => true,
364 Self::Overdraft { .. } => false,
365 Self::Other(err) => err.is_bad_transaction(),
366 Self::Eip2681 => true,
367 Self::Eip4844(eip4844_err) => {
368 match eip4844_err {
369 Eip4844PoolTransactionError::MissingEip4844BlobSidecar => {
370 // this is only reachable when blob transactions are reinjected and we're
371 // unable to find the previously extracted blob
372 false
373 }
374 Eip4844PoolTransactionError::InvalidEip4844Blob(_) => {
375 // This is only reachable when the blob is invalid
376 true
377 }
378 Eip4844PoolTransactionError::Eip4844NonceGap => {
379 // it is possible that the pool sees `nonce n` before `nonce n-1` and this
380 // is only thrown for valid(good) blob transactions
381 false
382 }
383 Eip4844PoolTransactionError::NoEip4844Blobs => {
384 // this is a malformed transaction and should not be sent over the network
385 true
386 }
387 Eip4844PoolTransactionError::TooManyEip4844Blobs { .. } => {
388 // this is a malformed transaction and should not be sent over the network
389 true
390 }
391 Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka |
392 Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka |
393 Eip4844PoolTransactionError::Eip7594SidecarDisallowed => {
394 // for now we do not want to penalize peers for broadcasting different
395 // sidecars
396 false
397 }
398 }
399 }
400 Self::Eip7702(eip7702_err) => match eip7702_err {
401 Eip7702PoolTransactionError::MissingEip7702AuthorizationList => {
402 // as EIP-7702 specifies, 7702 transactions must have an non-empty authorization
403 // list so this is a malformed transaction and should not be
404 // sent over the network
405 true
406 }
407 Eip7702PoolTransactionError::OutOfOrderTxFromDelegated => false,
408 Eip7702PoolTransactionError::InflightTxLimitReached => false,
409 Eip7702PoolTransactionError::AuthorityReserved => false,
410 },
411 Self::PriorityFeeBelowMinimum { .. } => false,
412 }
413 }
414
415 /// Returns `true` if this is a blob sidecar error (e.g. invalid proof, missing sidecar).
416 ///
417 /// These errors indicate the sidecar data from a specific peer was bad, but the transaction
418 /// hash itself may be valid when fetched from another peer.
419 #[inline]
420 pub const fn is_bad_blob_sidecar(&self) -> bool {
421 matches!(
422 self,
423 Self::Eip4844(
424 Eip4844PoolTransactionError::MissingEip4844BlobSidecar |
425 Eip4844PoolTransactionError::InvalidEip4844Blob(_) |
426 Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka |
427 Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka |
428 Eip4844PoolTransactionError::Eip7594SidecarDisallowed
429 )
430 )
431 }
432
433 /// Returns true if this is a [`Self::Consensus`] variant.
434 pub const fn as_consensus(&self) -> Option<&InvalidTransactionError> {
435 match self {
436 Self::Consensus(err) => Some(err),
437 _ => None,
438 }
439 }
440
441 /// Returns true if this is [`InvalidTransactionError::NonceNotConsistent`] and the
442 /// transaction's nonce is lower than the state's.
443 pub fn is_nonce_too_low(&self) -> bool {
444 match self {
445 Self::Consensus(err) => err.is_nonce_too_low(),
446 _ => false,
447 }
448 }
449
450 /// Returns `true` if an import failed due to an oversized transaction
451 pub const fn is_oversized(&self) -> bool {
452 matches!(self, Self::OversizedData { .. })
453 }
454
455 /// Returns `true` if an import failed due to nonce gap.
456 pub const fn is_nonce_gap(&self) -> bool {
457 matches!(self, Self::Consensus(InvalidTransactionError::NonceNotConsistent { .. })) ||
458 matches!(self, Self::Eip4844(Eip4844PoolTransactionError::Eip4844NonceGap))
459 }
460
461 /// Returns the arbitrary error if it is [`InvalidPoolTransactionError::Other`]
462 pub fn as_other(&self) -> Option<&dyn PoolTransactionError> {
463 match self {
464 Self::Other(err) => Some(&**err),
465 _ => None,
466 }
467 }
468
469 /// Returns a reference to the [`InvalidPoolTransactionError::Other`] value if this type is a
470 /// [`InvalidPoolTransactionError::Other`] of that type. Returns None otherwise.
471 pub fn downcast_other_ref<T: core::error::Error + 'static>(&self) -> Option<&T> {
472 let other = self.as_other()?;
473 other.as_any().downcast_ref()
474 }
475
476 /// Returns true if the this type is a [`InvalidPoolTransactionError::Other`] of that error
477 /// type. Returns false otherwise.
478 pub fn is_other<T: core::error::Error + 'static>(&self) -> bool {
479 self.as_other().map(|err| err.as_any().is::<T>()).unwrap_or(false)
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[derive(thiserror::Error, Debug)]
488 #[error("err")]
489 struct E;
490
491 impl PoolTransactionError for E {
492 fn is_bad_transaction(&self) -> bool {
493 false
494 }
495
496 fn as_any(&self) -> &dyn Any {
497 self
498 }
499 }
500
501 #[test]
502 fn other_downcast() {
503 let err = InvalidPoolTransactionError::Other(Box::new(E));
504 assert!(err.is_other::<E>());
505
506 assert!(err.downcast_other_ref::<E>().is_some());
507 }
508
509 #[test]
510 fn bad_blob_sidecar_detection() {
511 let err = PoolError::new(
512 TxHash::ZERO,
513 InvalidPoolTransactionError::Eip4844(Eip4844PoolTransactionError::InvalidEip4844Blob(
514 BlobTransactionValidationError::InvalidProof,
515 )),
516 );
517
518 assert!(err.is_bad_blob_sidecar());
519
520 let err = PoolError::new(
521 TxHash::ZERO,
522 InvalidPoolTransactionError::Eip4844(
523 Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
524 ),
525 );
526
527 assert!(err.is_bad_blob_sidecar());
528
529 let err = PoolError::new(
530 TxHash::ZERO,
531 InvalidPoolTransactionError::Eip4844(Eip4844PoolTransactionError::NoEip4844Blobs),
532 );
533
534 assert!(!err.is_bad_blob_sidecar());
535 }
536}