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