Skip to main content

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
150/// Represents all errors that can happen when validating transactions for the pool for EIP-4844
151/// transactions
152#[derive(Debug, thiserror::Error)]
153pub enum Eip4844PoolTransactionError {
154    /// Thrown if we're unable to find the blob for a transaction that was previously extracted
155    #[error("blob sidecar not found for EIP4844 transaction")]
156    MissingEip4844BlobSidecar,
157    /// Thrown if an EIP-4844 transaction without any blobs arrives
158    #[error("blobless blob transaction")]
159    NoEip4844Blobs,
160    /// Thrown if an EIP-4844 transaction arrives with too many blobs
161    #[error("too many blobs in transaction: have {have}, permitted {permitted}")]
162    TooManyEip4844Blobs {
163        /// Number of blobs the transaction has
164        have: u64,
165        /// Number of maximum blobs the transaction can have
166        permitted: u64,
167    },
168    /// Thrown if validating the blob sidecar for the transaction failed.
169    #[error(transparent)]
170    InvalidEip4844Blob(BlobTransactionValidationError),
171    /// EIP-4844 transactions are only accepted if they're gapless, meaning the previous nonce of
172    /// the transaction (`tx.nonce -1`) must either be in the pool or match the on chain nonce of
173    /// the sender.
174    ///
175    /// This error is thrown on validation if a valid blob transaction arrives with a nonce that
176    /// would introduce gap in the nonce sequence.
177    #[error("nonce too high")]
178    Eip4844NonceGap,
179    /// Thrown if blob transaction has an EIP-7594 style sidecar before Osaka.
180    #[error("unexpected eip-7594 sidecar before osaka")]
181    UnexpectedEip7594SidecarBeforeOsaka,
182    /// Thrown if blob transaction has an EIP-4844 style sidecar after Osaka.
183    #[error("unexpected eip-4844 sidecar after osaka")]
184    UnexpectedEip4844SidecarAfterOsaka,
185    /// Thrown if blob transaction has an EIP-7594 style sidecar but EIP-7594 support is disabled.
186    #[error("eip-7594 sidecar disallowed")]
187    Eip7594SidecarDisallowed,
188}
189
190/// Represents all errors that can happen when validating transactions for the pool for EIP-7702
191/// transactions
192#[derive(Debug, thiserror::Error)]
193pub enum Eip7702PoolTransactionError {
194    /// Thrown if the transaction has no items in its authorization list
195    #[error("no items in authorization list for EIP7702 transaction")]
196    MissingEip7702AuthorizationList,
197    /// Returned when a transaction with a nonce
198    /// gap is received from accounts with a deployed delegation or pending delegation.
199    #[error("gapped-nonce tx from delegated accounts")]
200    OutOfOrderTxFromDelegated,
201    /// Returned when the maximum number of in-flight
202    /// transactions is reached for specific accounts.
203    #[error("in-flight transaction limit reached for delegated accounts")]
204    InflightTxLimitReached,
205    /// Returned if a transaction has an authorization
206    /// signed by an address which already has in-flight transactions known to the
207    /// pool.
208    #[error("authority already reserved")]
209    AuthorityReserved,
210}
211
212/// Represents errors that can happen when validating transactions for the pool
213///
214/// See [`TransactionValidator`](crate::TransactionValidator).
215#[derive(Debug, thiserror::Error)]
216pub enum InvalidPoolTransactionError {
217    /// Hard consensus errors
218    #[error(transparent)]
219    Consensus(#[from] InvalidTransactionError),
220    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
221    /// respect the size limits of the pool.
222    #[error("transaction's gas limit {0} exceeds block's gas limit {1}")]
223    ExceedsGasLimit(u64, u64),
224    /// Thrown when a transaction's gas limit exceeds the configured maximum per-transaction limit.
225    #[error("transaction's gas limit {0} exceeds maximum per-transaction gas limit {1}")]
226    MaxTxGasLimitExceeded(u64, u64),
227    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
228    /// respect the tx fee exceeds the configured cap
229    #[error("tx fee ({max_tx_fee_wei} wei) exceeds the configured cap ({tx_fee_cap_wei} wei)")]
230    ExceedsFeeCap {
231        /// max fee in wei of new tx submitted to the pool (e.g. 0.11534 ETH)
232        max_tx_fee_wei: u128,
233        /// configured tx fee cap in wei (e.g. 1.0 ETH)
234        tx_fee_cap_wei: u128,
235    },
236    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
237    /// respect the `max_init_code_size`.
238    #[error("transaction's input size {0} exceeds max_init_code_size {1}")]
239    ExceedsMaxInitCodeSize(usize, usize),
240    /// Thrown if the input data of a transaction is greater
241    /// than some meaningful limit a user might use. This is not a consensus error
242    /// making the transaction invalid, rather a DOS protection.
243    #[error("oversized data: transaction size {size}, limit {limit}")]
244    OversizedData {
245        /// Size of the transaction/input data that exceeded the limit.
246        size: usize,
247        /// Configured limit that was exceeded.
248        limit: usize,
249    },
250    /// Thrown if the transaction's fee is below the minimum fee
251    #[error("transaction underpriced")]
252    Underpriced,
253    /// Thrown if the transaction's would require an account to be overdrawn
254    #[error("transaction overdraws from account, balance: {balance}, cost: {cost}")]
255    Overdraft {
256        /// Cost transaction is allowed to consume. See `reth_transaction_pool::PoolTransaction`.
257        cost: U256,
258        /// Balance of account.
259        balance: U256,
260    },
261    /// EIP-2681 error thrown if the nonce is higher or equal than `U64::max`
262    /// `<https://eips.ethereum.org/EIPS/eip-2681>`
263    #[error("nonce exceeds u64 limit")]
264    Eip2681,
265    /// EIP-4844 related errors
266    #[error(transparent)]
267    Eip4844(#[from] Eip4844PoolTransactionError),
268    /// EIP-7702 related errors
269    #[error(transparent)]
270    Eip7702(#[from] Eip7702PoolTransactionError),
271    /// Any other error that occurred while inserting/validating that is transaction specific
272    #[error(transparent)]
273    Other(Box<dyn PoolTransactionError>),
274    /// The transaction is specified to use less gas than required to start the
275    /// invocation.
276    #[error("intrinsic gas too low")]
277    IntrinsicGasTooLow,
278    /// The transaction priority fee is below the minimum required priority fee.
279    #[error("transaction priority fee below minimum required priority fee {minimum_priority_fee}")]
280    PriorityFeeBelowMinimum {
281        /// Minimum required priority fee.
282        minimum_priority_fee: u128,
283    },
284}
285
286// === impl InvalidPoolTransactionError ===
287
288impl InvalidPoolTransactionError {
289    /// Returns a new [`InvalidPoolTransactionError::Other`] instance with the given
290    /// [`PoolTransactionError`].
291    pub fn other<E: PoolTransactionError + 'static>(err: E) -> Self {
292        Self::Other(Box::new(err))
293    }
294
295    /// Returns `true` if the error was caused by a transaction that is considered bad in the
296    /// context of the transaction pool and warrants peer penalization.
297    ///
298    /// See [`PoolError::is_bad_transaction`].
299    #[expect(clippy::match_same_arms)]
300    #[inline]
301    fn is_bad_transaction(&self) -> bool {
302        match self {
303            Self::Consensus(err) => {
304                // transaction considered invalid by the consensus rules
305                // We do not consider the following errors to be erroneous transactions, since they
306                // depend on dynamic environmental conditions and should not be assumed to have been
307                // intentionally caused by the sender
308                match err {
309                    InvalidTransactionError::InsufficientFunds { .. } |
310                    InvalidTransactionError::NonceNotConsistent { .. } => {
311                        // transaction could just have arrived late/early
312                        false
313                    }
314                    InvalidTransactionError::GasTooLow |
315                    InvalidTransactionError::GasTooHigh |
316                    InvalidTransactionError::TipAboveFeeCap => {
317                        // these are technically not invalid
318                        false
319                    }
320                    InvalidTransactionError::FeeCapTooLow => {
321                        // dynamic, but not used during validation
322                        false
323                    }
324                    InvalidTransactionError::Eip2930Disabled |
325                    InvalidTransactionError::Eip1559Disabled |
326                    InvalidTransactionError::Eip4844Disabled |
327                    InvalidTransactionError::Eip7702Disabled => {
328                        // settings
329                        false
330                    }
331                    InvalidTransactionError::OldLegacyChainId |
332                    InvalidTransactionError::ChainIdMismatch |
333                    InvalidTransactionError::GasUintOverflow |
334                    InvalidTransactionError::TxTypeNotSupported |
335                    InvalidTransactionError::SignerAccountHasBytecode |
336                    InvalidTransactionError::GasLimitTooHigh => true,
337                }
338            }
339            Self::ExceedsGasLimit(_, _) => true,
340            Self::MaxTxGasLimitExceeded(_, _) => {
341                // local setting
342                false
343            }
344            Self::ExceedsFeeCap { max_tx_fee_wei: _, tx_fee_cap_wei: _ } => true,
345            Self::ExceedsMaxInitCodeSize(_, _) => true,
346            Self::OversizedData { .. } => true,
347            Self::Underpriced => {
348                // local setting
349                false
350            }
351            Self::IntrinsicGasTooLow => true,
352            Self::Overdraft { .. } => false,
353            Self::Other(err) => err.is_bad_transaction(),
354            Self::Eip2681 => true,
355            Self::Eip4844(eip4844_err) => {
356                match eip4844_err {
357                    Eip4844PoolTransactionError::MissingEip4844BlobSidecar => {
358                        // this is only reachable when blob transactions are reinjected and we're
359                        // unable to find the previously extracted blob
360                        false
361                    }
362                    Eip4844PoolTransactionError::InvalidEip4844Blob(_) => {
363                        // This is only reachable when the blob is invalid
364                        true
365                    }
366                    Eip4844PoolTransactionError::Eip4844NonceGap => {
367                        // it is possible that the pool sees `nonce n` before `nonce n-1` and this
368                        // is only thrown for valid(good) blob transactions
369                        false
370                    }
371                    Eip4844PoolTransactionError::NoEip4844Blobs => {
372                        // this is a malformed transaction and should not be sent over the network
373                        true
374                    }
375                    Eip4844PoolTransactionError::TooManyEip4844Blobs { .. } => {
376                        // this is a malformed transaction and should not be sent over the network
377                        true
378                    }
379                    Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka |
380                    Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka |
381                    Eip4844PoolTransactionError::Eip7594SidecarDisallowed => {
382                        // for now we do not want to penalize peers for broadcasting different
383                        // sidecars
384                        false
385                    }
386                }
387            }
388            Self::Eip7702(eip7702_err) => match eip7702_err {
389                Eip7702PoolTransactionError::MissingEip7702AuthorizationList => {
390                    // as EIP-7702 specifies, 7702 transactions must have an non-empty authorization
391                    // list so this is a malformed transaction and should not be
392                    // sent over the network
393                    true
394                }
395                Eip7702PoolTransactionError::OutOfOrderTxFromDelegated => false,
396                Eip7702PoolTransactionError::InflightTxLimitReached => false,
397                Eip7702PoolTransactionError::AuthorityReserved => false,
398            },
399            Self::PriorityFeeBelowMinimum { .. } => false,
400        }
401    }
402
403    /// Returns true if this is a [`Self::Consensus`] variant.
404    pub const fn as_consensus(&self) -> Option<&InvalidTransactionError> {
405        match self {
406            Self::Consensus(err) => Some(err),
407            _ => None,
408        }
409    }
410
411    /// Returns true if this is [`InvalidTransactionError::NonceNotConsistent`] and the
412    /// transaction's nonce is lower than the state's.
413    pub fn is_nonce_too_low(&self) -> bool {
414        match self {
415            Self::Consensus(err) => err.is_nonce_too_low(),
416            _ => false,
417        }
418    }
419
420    /// Returns `true` if an import failed due to an oversized transaction
421    pub const fn is_oversized(&self) -> bool {
422        matches!(self, Self::OversizedData { .. })
423    }
424
425    /// Returns `true` if an import failed due to nonce gap.
426    pub const fn is_nonce_gap(&self) -> bool {
427        matches!(self, Self::Consensus(InvalidTransactionError::NonceNotConsistent { .. })) ||
428            matches!(self, Self::Eip4844(Eip4844PoolTransactionError::Eip4844NonceGap))
429    }
430
431    /// Returns the arbitrary error if it is [`InvalidPoolTransactionError::Other`]
432    pub fn as_other(&self) -> Option<&dyn PoolTransactionError> {
433        match self {
434            Self::Other(err) => Some(&**err),
435            _ => None,
436        }
437    }
438
439    /// Returns a reference to the [`InvalidPoolTransactionError::Other`] value if this type is a
440    /// [`InvalidPoolTransactionError::Other`] of that type. Returns None otherwise.
441    pub fn downcast_other_ref<T: core::error::Error + 'static>(&self) -> Option<&T> {
442        let other = self.as_other()?;
443        other.as_any().downcast_ref()
444    }
445
446    /// Returns true if the this type is a [`InvalidPoolTransactionError::Other`] of that error
447    /// type. Returns false otherwise.
448    pub fn is_other<T: core::error::Error + 'static>(&self) -> bool {
449        self.as_other().map(|err| err.as_any().is::<T>()).unwrap_or(false)
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[derive(thiserror::Error, Debug)]
458    #[error("err")]
459    struct E;
460
461    impl PoolTransactionError for E {
462        fn is_bad_transaction(&self) -> bool {
463            false
464        }
465
466        fn as_any(&self) -> &dyn Any {
467            self
468        }
469    }
470
471    #[test]
472    fn other_downcast() {
473        let err = InvalidPoolTransactionError::Other(Box::new(E));
474        assert!(err.is_other::<E>());
475
476        assert!(err.downcast_other_ref::<E>().is_some());
477    }
478}