reth_transaction_pool/
error.rs

1//! Transaction pool errors
2
3use alloy_eips::eip4844::BlobTransactionValidationError;
4use alloy_primitives::{Address, TxHash, U256};
5use reth_primitives::InvalidTransactionError;
6
7/// Transaction pool result type.
8pub type PoolResult<T> = Result<T, PoolError>;
9
10/// A trait for additional errors that can be thrown by the transaction pool.
11///
12/// For example during validation
13/// [`TransactionValidator::validate_transaction`](crate::validate::TransactionValidator::validate_transaction)
14pub trait PoolTransactionError: core::error::Error + Send + Sync {
15    /// Returns `true` if the error was caused by a transaction that is considered bad in the
16    /// context of the transaction pool and warrants peer penalization.
17    ///
18    /// See [`PoolError::is_bad_transaction`].
19    fn is_bad_transaction(&self) -> bool;
20}
21
22// Needed for `#[error(transparent)]`
23impl core::error::Error for Box<dyn PoolTransactionError> {
24    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
25        (**self).source()
26    }
27}
28
29/// Transaction pool error.
30#[derive(Debug, thiserror::Error)]
31#[error("[{hash}]: {kind}")]
32pub struct PoolError {
33    /// The transaction hash that caused the error.
34    pub hash: TxHash,
35    /// The error kind.
36    pub kind: PoolErrorKind,
37}
38
39/// Transaction pool error kind.
40#[derive(Debug, thiserror::Error)]
41pub enum PoolErrorKind {
42    /// Same transaction already imported
43    #[error("already imported")]
44    AlreadyImported,
45    /// Thrown if a replacement transaction's gas price is below the already imported transaction
46    #[error("insufficient gas price to replace existing transaction")]
47    ReplacementUnderpriced,
48    /// The fee cap of the transaction is below the minimum fee cap determined by the protocol
49    #[error("transaction feeCap {0} below chain minimum")]
50    FeeCapBelowMinimumProtocolFeeCap(u128),
51    /// Thrown when the number of unique transactions of a sender exceeded the slot capacity.
52    #[error("rejected due to {0} being identified as a spammer")]
53    SpammerExceededCapacity(Address),
54    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
55    /// respect the size limits of the pool.
56    #[error("transaction discarded outright due to pool size constraints")]
57    DiscardedOnInsert,
58    /// Thrown when the transaction is considered invalid.
59    #[error(transparent)]
60    InvalidTransaction(#[from] InvalidPoolTransactionError),
61    /// Thrown if the mutual exclusivity constraint (blob vs normal transaction) is violated.
62    #[error("transaction type {1} conflicts with existing transaction for {0}")]
63    ExistingConflictingTransactionType(Address, u8),
64    /// Any other error that occurred while inserting/validating a transaction. e.g. IO database
65    /// error
66    #[error(transparent)]
67    Other(#[from] Box<dyn core::error::Error + Send + Sync>),
68}
69
70// === impl PoolError ===
71
72impl PoolError {
73    /// Creates a new pool error.
74    pub fn new(hash: TxHash, kind: impl Into<PoolErrorKind>) -> Self {
75        Self { hash, kind: kind.into() }
76    }
77
78    /// Creates a new pool error with the `Other` kind.
79    pub fn other(
80        hash: TxHash,
81        error: impl Into<Box<dyn core::error::Error + Send + Sync>>,
82    ) -> Self {
83        Self { hash, kind: PoolErrorKind::Other(error.into()) }
84    }
85
86    /// Returns `true` if the error was caused by a transaction that is considered bad in the
87    /// context of the transaction pool and warrants peer penalization.
88    ///
89    /// Not all error variants are caused by the incorrect composition of the transaction (See also
90    /// [`InvalidPoolTransactionError`]) and can be caused by the current state of the transaction
91    /// pool. For example the transaction pool is already full or the error was caused my an
92    /// internal error, such as database errors.
93    ///
94    /// This function returns true only if the transaction will never make it into the pool because
95    /// its composition is invalid and the original sender should have detected this as well. This
96    /// is used to determine whether the original sender should be penalized for sending an
97    /// erroneous transaction.
98    #[inline]
99    pub fn is_bad_transaction(&self) -> bool {
100        #[allow(clippy::match_same_arms)]
101        match &self.kind {
102            PoolErrorKind::AlreadyImported => {
103                // already imported but not bad
104                false
105            }
106            PoolErrorKind::ReplacementUnderpriced => {
107                // already imported but not bad
108                false
109            }
110            PoolErrorKind::FeeCapBelowMinimumProtocolFeeCap(_) => {
111                // fee cap of the tx below the technical minimum determined by the protocol, see
112                // [MINIMUM_PROTOCOL_FEE_CAP](alloy_primitives::constants::MIN_PROTOCOL_BASE_FEE)
113                // although this transaction will always be invalid, we do not want to penalize the
114                // sender because this check simply could not be implemented by the client
115                false
116            }
117            PoolErrorKind::SpammerExceededCapacity(_) => {
118                // the sender exceeded the slot capacity, we should not penalize the peer for
119                // sending the tx because we don't know if all the transactions are sent from the
120                // same peer, there's also a chance that old transactions haven't been cleared yet
121                // (pool lags behind) and old transaction still occupy a slot in the pool
122                false
123            }
124            PoolErrorKind::DiscardedOnInsert => {
125                // valid tx but dropped due to size constraints
126                false
127            }
128            PoolErrorKind::InvalidTransaction(err) => {
129                // transaction rejected because it violates constraints
130                err.is_bad_transaction()
131            }
132            PoolErrorKind::Other(_) => {
133                // internal error unrelated to the transaction
134                false
135            }
136            PoolErrorKind::ExistingConflictingTransactionType(_, _) => {
137                // this is not a protocol error but an implementation error since the pool enforces
138                // exclusivity (blob vs normal tx) for all senders
139                false
140            }
141        }
142    }
143}
144
145/// Represents all errors that can happen when validating transactions for the pool for EIP-4844
146/// transactions
147#[derive(Debug, thiserror::Error)]
148pub enum Eip4844PoolTransactionError {
149    /// Thrown if we're unable to find the blob for a transaction that was previously extracted
150    #[error("blob sidecar not found for EIP4844 transaction")]
151    MissingEip4844BlobSidecar,
152    /// Thrown if an EIP-4844 transaction without any blobs arrives
153    #[error("blobless blob transaction")]
154    NoEip4844Blobs,
155    /// Thrown if an EIP-4844 transaction without any blobs arrives
156    #[error("too many blobs in transaction: have {have}, permitted {permitted}")]
157    TooManyEip4844Blobs {
158        /// Number of blobs the transaction has
159        have: usize,
160        /// Number of maximum blobs the transaction can have
161        permitted: usize,
162    },
163    /// Thrown if validating the blob sidecar for the transaction failed.
164    #[error(transparent)]
165    InvalidEip4844Blob(BlobTransactionValidationError),
166    /// EIP-4844 transactions are only accepted if they're gapless, meaning the previous nonce of
167    /// the transaction (`tx.nonce -1`) must either be in the pool or match the on chain nonce of
168    /// the sender.
169    ///
170    /// This error is thrown on validation if a valid blob transaction arrives with a nonce that
171    /// would introduce gap in the nonce sequence.
172    #[error("nonce too high")]
173    Eip4844NonceGap,
174}
175
176/// Represents all errors that can happen when validating transactions for the pool for EIP-7702
177/// transactions
178#[derive(Debug, thiserror::Error)]
179pub enum Eip7702PoolTransactionError {
180    /// Thrown if the transaction has no items in its authorization list
181    #[error("no items in authorization list for EIP7702 transaction")]
182    MissingEip7702AuthorizationList,
183}
184
185/// Represents errors that can happen when validating transactions for the pool
186///
187/// See [`TransactionValidator`](crate::TransactionValidator).
188#[derive(Debug, thiserror::Error)]
189pub enum InvalidPoolTransactionError {
190    /// Hard consensus errors
191    #[error(transparent)]
192    Consensus(#[from] InvalidTransactionError),
193    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
194    /// respect the size limits of the pool.
195    #[error("transaction's gas limit {0} exceeds block's gas limit {1}")]
196    ExceedsGasLimit(u64, u64),
197    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
198    /// respect the `max_init_code_size`.
199    #[error("transaction's size {0} exceeds max_init_code_size {1}")]
200    ExceedsMaxInitCodeSize(usize, usize),
201    /// Thrown if the input data of a transaction is greater
202    /// than some meaningful limit a user might use. This is not a consensus error
203    /// making the transaction invalid, rather a DOS protection.
204    #[error("input data too large")]
205    OversizedData(usize, usize),
206    /// Thrown if the transaction's fee is below the minimum fee
207    #[error("transaction underpriced")]
208    Underpriced,
209    /// Thrown if the transaction's would require an account to be overdrawn
210    #[error("transaction overdraws from account, balance: {balance}, cost: {cost}")]
211    Overdraft {
212        /// Cost transaction is allowed to consume. See `reth_transaction_pool::PoolTransaction`.
213        cost: U256,
214        /// Balance of account.
215        balance: U256,
216    },
217    /// EIP-4844 related errors
218    #[error(transparent)]
219    Eip4844(#[from] Eip4844PoolTransactionError),
220    /// EIP-7702 related errors
221    #[error(transparent)]
222    Eip7702(#[from] Eip7702PoolTransactionError),
223    /// Any other error that occurred while inserting/validating that is transaction specific
224    #[error(transparent)]
225    Other(Box<dyn PoolTransactionError>),
226    /// The transaction is specified to use less gas than required to start the
227    /// invocation.
228    #[error("intrinsic gas too low")]
229    IntrinsicGasTooLow,
230}
231
232// === impl InvalidPoolTransactionError ===
233
234impl InvalidPoolTransactionError {
235    /// Returns `true` if the error was caused by a transaction that is considered bad in the
236    /// context of the transaction pool and warrants peer penalization.
237    ///
238    /// See [`PoolError::is_bad_transaction`].
239    #[allow(clippy::match_same_arms)]
240    #[inline]
241    fn is_bad_transaction(&self) -> bool {
242        match self {
243            Self::Consensus(err) => {
244                // transaction considered invalid by the consensus rules
245                // We do not consider the following errors to be erroneous transactions, since they
246                // depend on dynamic environmental conditions and should not be assumed to have been
247                // intentionally caused by the sender
248                match err {
249                    InvalidTransactionError::InsufficientFunds { .. } |
250                    InvalidTransactionError::NonceNotConsistent { .. } => {
251                        // transaction could just have arrived late/early
252                        false
253                    }
254                    InvalidTransactionError::GasTooLow |
255                    InvalidTransactionError::GasTooHigh |
256                    InvalidTransactionError::TipAboveFeeCap => {
257                        // these are technically not invalid
258                        false
259                    }
260                    InvalidTransactionError::FeeCapTooLow => {
261                        // dynamic, but not used during validation
262                        false
263                    }
264                    InvalidTransactionError::Eip2930Disabled |
265                    InvalidTransactionError::Eip1559Disabled |
266                    InvalidTransactionError::Eip4844Disabled |
267                    InvalidTransactionError::Eip7702Disabled => {
268                        // settings
269                        false
270                    }
271                    InvalidTransactionError::OldLegacyChainId |
272                    InvalidTransactionError::ChainIdMismatch |
273                    InvalidTransactionError::GasUintOverflow |
274                    InvalidTransactionError::TxTypeNotSupported |
275                    InvalidTransactionError::SignerAccountHasBytecode => true,
276                }
277            }
278            Self::ExceedsGasLimit(_, _) => true,
279            Self::ExceedsMaxInitCodeSize(_, _) => true,
280            Self::OversizedData(_, _) => true,
281            Self::Underpriced => {
282                // local setting
283                false
284            }
285            Self::IntrinsicGasTooLow => true,
286            Self::Overdraft { .. } => false,
287            Self::Other(err) => err.is_bad_transaction(),
288            Self::Eip4844(eip4844_err) => {
289                match eip4844_err {
290                    Eip4844PoolTransactionError::MissingEip4844BlobSidecar => {
291                        // this is only reachable when blob transactions are reinjected and we're
292                        // unable to find the previously extracted blob
293                        false
294                    }
295                    Eip4844PoolTransactionError::InvalidEip4844Blob(_) => {
296                        // This is only reachable when the blob is invalid
297                        true
298                    }
299                    Eip4844PoolTransactionError::Eip4844NonceGap => {
300                        // it is possible that the pool sees `nonce n` before `nonce n-1` and this
301                        // is only thrown for valid(good) blob transactions
302                        false
303                    }
304                    Eip4844PoolTransactionError::NoEip4844Blobs => {
305                        // this is a malformed transaction and should not be sent over the network
306                        true
307                    }
308                    Eip4844PoolTransactionError::TooManyEip4844Blobs { .. } => {
309                        // this is a malformed transaction and should not be sent over the network
310                        true
311                    }
312                }
313            }
314            Self::Eip7702(eip7702_err) => match eip7702_err {
315                Eip7702PoolTransactionError::MissingEip7702AuthorizationList => false,
316            },
317        }
318    }
319
320    /// Returns `true` if an import failed due to nonce gap.
321    pub const fn is_nonce_gap(&self) -> bool {
322        matches!(self, Self::Consensus(InvalidTransactionError::NonceNotConsistent { .. })) ||
323            matches!(self, Self::Eip4844(Eip4844PoolTransactionError::Eip4844NonceGap))
324    }
325}