reth_transaction_pool/
error.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
//! Transaction pool errors

use alloy_eips::eip4844::BlobTransactionValidationError;
use alloy_primitives::{Address, TxHash, U256};
use reth_primitives::InvalidTransactionError;

/// Transaction pool result type.
pub type PoolResult<T> = Result<T, PoolError>;

/// A trait for additional errors that can be thrown by the transaction pool.
///
/// For example during validation
/// [`TransactionValidator::validate_transaction`](crate::validate::TransactionValidator::validate_transaction)
pub trait PoolTransactionError: core::error::Error + Send + Sync {
    /// Returns `true` if the error was caused by a transaction that is considered bad in the
    /// context of the transaction pool and warrants peer penalization.
    ///
    /// See [`PoolError::is_bad_transaction`].
    fn is_bad_transaction(&self) -> bool;
}

// Needed for `#[error(transparent)]`
impl core::error::Error for Box<dyn PoolTransactionError> {
    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
        (**self).source()
    }
}

/// Transaction pool error.
#[derive(Debug, thiserror::Error)]
#[error("[{hash}]: {kind}")]
pub struct PoolError {
    /// The transaction hash that caused the error.
    pub hash: TxHash,
    /// The error kind.
    pub kind: PoolErrorKind,
}

/// Transaction pool error kind.
#[derive(Debug, thiserror::Error)]
pub enum PoolErrorKind {
    /// Same transaction already imported
    #[error("already imported")]
    AlreadyImported,
    /// Thrown if a replacement transaction's gas price is below the already imported transaction
    #[error("insufficient gas price to replace existing transaction")]
    ReplacementUnderpriced,
    /// The fee cap of the transaction is below the minimum fee cap determined by the protocol
    #[error("transaction feeCap {0} below chain minimum")]
    FeeCapBelowMinimumProtocolFeeCap(u128),
    /// Thrown when the number of unique transactions of a sender exceeded the slot capacity.
    #[error("rejected due to {0} being identified as a spammer")]
    SpammerExceededCapacity(Address),
    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
    /// respect the size limits of the pool.
    #[error("transaction discarded outright due to pool size constraints")]
    DiscardedOnInsert,
    /// Thrown when the transaction is considered invalid.
    #[error(transparent)]
    InvalidTransaction(#[from] InvalidPoolTransactionError),
    /// Thrown if the mutual exclusivity constraint (blob vs normal transaction) is violated.
    #[error("transaction type {1} conflicts with existing transaction for {0}")]
    ExistingConflictingTransactionType(Address, u8),
    /// Any other error that occurred while inserting/validating a transaction. e.g. IO database
    /// error
    #[error(transparent)]
    Other(#[from] Box<dyn core::error::Error + Send + Sync>),
}

// === impl PoolError ===

impl PoolError {
    /// Creates a new pool error.
    pub fn new(hash: TxHash, kind: impl Into<PoolErrorKind>) -> Self {
        Self { hash, kind: kind.into() }
    }

    /// Creates a new pool error with the `Other` kind.
    pub fn other(
        hash: TxHash,
        error: impl Into<Box<dyn core::error::Error + Send + Sync>>,
    ) -> Self {
        Self { hash, kind: PoolErrorKind::Other(error.into()) }
    }

    /// Returns `true` if the error was caused by a transaction that is considered bad in the
    /// context of the transaction pool and warrants peer penalization.
    ///
    /// Not all error variants are caused by the incorrect composition of the transaction (See also
    /// [`InvalidPoolTransactionError`]) and can be caused by the current state of the transaction
    /// pool. For example the transaction pool is already full or the error was caused my an
    /// internal error, such as database errors.
    ///
    /// This function returns true only if the transaction will never make it into the pool because
    /// its composition is invalid and the original sender should have detected this as well. This
    /// is used to determine whether the original sender should be penalized for sending an
    /// erroneous transaction.
    #[inline]
    pub fn is_bad_transaction(&self) -> bool {
        #[allow(clippy::match_same_arms)]
        match &self.kind {
            PoolErrorKind::AlreadyImported => {
                // already imported but not bad
                false
            }
            PoolErrorKind::ReplacementUnderpriced => {
                // already imported but not bad
                false
            }
            PoolErrorKind::FeeCapBelowMinimumProtocolFeeCap(_) => {
                // fee cap of the tx below the technical minimum determined by the protocol, see
                // [MINIMUM_PROTOCOL_FEE_CAP](alloy_primitives::constants::MIN_PROTOCOL_BASE_FEE)
                // although this transaction will always be invalid, we do not want to penalize the
                // sender because this check simply could not be implemented by the client
                false
            }
            PoolErrorKind::SpammerExceededCapacity(_) => {
                // the sender exceeded the slot capacity, we should not penalize the peer for
                // sending the tx because we don't know if all the transactions are sent from the
                // same peer, there's also a chance that old transactions haven't been cleared yet
                // (pool lags behind) and old transaction still occupy a slot in the pool
                false
            }
            PoolErrorKind::DiscardedOnInsert => {
                // valid tx but dropped due to size constraints
                false
            }
            PoolErrorKind::InvalidTransaction(err) => {
                // transaction rejected because it violates constraints
                err.is_bad_transaction()
            }
            PoolErrorKind::Other(_) => {
                // internal error unrelated to the transaction
                false
            }
            PoolErrorKind::ExistingConflictingTransactionType(_, _) => {
                // this is not a protocol error but an implementation error since the pool enforces
                // exclusivity (blob vs normal tx) for all senders
                false
            }
        }
    }
}

/// Represents all errors that can happen when validating transactions for the pool for EIP-4844
/// transactions
#[derive(Debug, thiserror::Error)]
pub enum Eip4844PoolTransactionError {
    /// Thrown if we're unable to find the blob for a transaction that was previously extracted
    #[error("blob sidecar not found for EIP4844 transaction")]
    MissingEip4844BlobSidecar,
    /// Thrown if an EIP-4844 transaction without any blobs arrives
    #[error("blobless blob transaction")]
    NoEip4844Blobs,
    /// Thrown if an EIP-4844 transaction without any blobs arrives
    #[error("too many blobs in transaction: have {have}, permitted {permitted}")]
    TooManyEip4844Blobs {
        /// Number of blobs the transaction has
        have: usize,
        /// Number of maximum blobs the transaction can have
        permitted: usize,
    },
    /// Thrown if validating the blob sidecar for the transaction failed.
    #[error(transparent)]
    InvalidEip4844Blob(BlobTransactionValidationError),
    /// EIP-4844 transactions are only accepted if they're gapless, meaning the previous nonce of
    /// the transaction (`tx.nonce -1`) must either be in the pool or match the on chain nonce of
    /// the sender.
    ///
    /// This error is thrown on validation if a valid blob transaction arrives with a nonce that
    /// would introduce gap in the nonce sequence.
    #[error("nonce too high")]
    Eip4844NonceGap,
}

/// Represents all errors that can happen when validating transactions for the pool for EIP-7702
/// transactions
#[derive(Debug, thiserror::Error)]
pub enum Eip7702PoolTransactionError {
    /// Thrown if the transaction has no items in its authorization list
    #[error("no items in authorization list for EIP7702 transaction")]
    MissingEip7702AuthorizationList,
}

/// Represents errors that can happen when validating transactions for the pool
///
/// See [`TransactionValidator`](crate::TransactionValidator).
#[derive(Debug, thiserror::Error)]
pub enum InvalidPoolTransactionError {
    /// Hard consensus errors
    #[error(transparent)]
    Consensus(#[from] InvalidTransactionError),
    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
    /// respect the size limits of the pool.
    #[error("transaction's gas limit {0} exceeds block's gas limit {1}")]
    ExceedsGasLimit(u64, u64),
    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
    /// respect the `max_init_code_size`.
    #[error("transaction's size {0} exceeds max_init_code_size {1}")]
    ExceedsMaxInitCodeSize(usize, usize),
    /// Thrown if the input data of a transaction is greater
    /// than some meaningful limit a user might use. This is not a consensus error
    /// making the transaction invalid, rather a DOS protection.
    #[error("input data too large")]
    OversizedData(usize, usize),
    /// Thrown if the transaction's fee is below the minimum fee
    #[error("transaction underpriced")]
    Underpriced,
    /// Thrown if the transaction's would require an account to be overdrawn
    #[error("transaction overdraws from account, balance: {balance}, cost: {cost}")]
    Overdraft {
        /// Cost transaction is allowed to consume. See `reth_transaction_pool::PoolTransaction`.
        cost: U256,
        /// Balance of account.
        balance: U256,
    },
    /// EIP-4844 related errors
    #[error(transparent)]
    Eip4844(#[from] Eip4844PoolTransactionError),
    /// EIP-7702 related errors
    #[error(transparent)]
    Eip7702(#[from] Eip7702PoolTransactionError),
    /// Any other error that occurred while inserting/validating that is transaction specific
    #[error(transparent)]
    Other(Box<dyn PoolTransactionError>),
    /// The transaction is specified to use less gas than required to start the
    /// invocation.
    #[error("intrinsic gas too low")]
    IntrinsicGasTooLow,
}

// === impl InvalidPoolTransactionError ===

impl InvalidPoolTransactionError {
    /// Returns `true` if the error was caused by a transaction that is considered bad in the
    /// context of the transaction pool and warrants peer penalization.
    ///
    /// See [`PoolError::is_bad_transaction`].
    #[allow(clippy::match_same_arms)]
    #[inline]
    fn is_bad_transaction(&self) -> bool {
        match self {
            Self::Consensus(err) => {
                // transaction considered invalid by the consensus rules
                // We do not consider the following errors to be erroneous transactions, since they
                // depend on dynamic environmental conditions and should not be assumed to have been
                // intentionally caused by the sender
                match err {
                    InvalidTransactionError::InsufficientFunds { .. } |
                    InvalidTransactionError::NonceNotConsistent { .. } => {
                        // transaction could just have arrived late/early
                        false
                    }
                    InvalidTransactionError::GasTooLow |
                    InvalidTransactionError::GasTooHigh |
                    InvalidTransactionError::TipAboveFeeCap => {
                        // these are technically not invalid
                        false
                    }
                    InvalidTransactionError::FeeCapTooLow => {
                        // dynamic, but not used during validation
                        false
                    }
                    InvalidTransactionError::Eip2930Disabled |
                    InvalidTransactionError::Eip1559Disabled |
                    InvalidTransactionError::Eip4844Disabled |
                    InvalidTransactionError::Eip7702Disabled => {
                        // settings
                        false
                    }
                    InvalidTransactionError::OldLegacyChainId |
                    InvalidTransactionError::ChainIdMismatch |
                    InvalidTransactionError::GasUintOverflow |
                    InvalidTransactionError::TxTypeNotSupported |
                    InvalidTransactionError::SignerAccountHasBytecode => true,
                }
            }
            Self::ExceedsGasLimit(_, _) => true,
            Self::ExceedsMaxInitCodeSize(_, _) => true,
            Self::OversizedData(_, _) => true,
            Self::Underpriced => {
                // local setting
                false
            }
            Self::IntrinsicGasTooLow => true,
            Self::Overdraft { .. } => false,
            Self::Other(err) => err.is_bad_transaction(),
            Self::Eip4844(eip4844_err) => {
                match eip4844_err {
                    Eip4844PoolTransactionError::MissingEip4844BlobSidecar => {
                        // this is only reachable when blob transactions are reinjected and we're
                        // unable to find the previously extracted blob
                        false
                    }
                    Eip4844PoolTransactionError::InvalidEip4844Blob(_) => {
                        // This is only reachable when the blob is invalid
                        true
                    }
                    Eip4844PoolTransactionError::Eip4844NonceGap => {
                        // it is possible that the pool sees `nonce n` before `nonce n-1` and this
                        // is only thrown for valid(good) blob transactions
                        false
                    }
                    Eip4844PoolTransactionError::NoEip4844Blobs => {
                        // this is a malformed transaction and should not be sent over the network
                        true
                    }
                    Eip4844PoolTransactionError::TooManyEip4844Blobs { .. } => {
                        // this is a malformed transaction and should not be sent over the network
                        true
                    }
                }
            }
            Self::Eip7702(eip7702_err) => match eip7702_err {
                Eip7702PoolTransactionError::MissingEip7702AuthorizationList => false,
            },
        }
    }

    /// Returns `true` if an import failed due to nonce gap.
    pub const fn is_nonce_gap(&self) -> bool {
        matches!(self, Self::Consensus(InvalidTransactionError::NonceNotConsistent { .. })) ||
            matches!(self, Self::Eip4844(Eip4844PoolTransactionError::Eip4844NonceGap))
    }
}