Files
nanoreth/crates/transaction-pool/src/error.rs

316 lines
14 KiB
Rust

//! Transaction pool errors
use reth_primitives::{Address, BlobTransactionValidationError, InvalidTransactionError, TxHash};
/// 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: std::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 std::error::Error for Box<dyn PoolTransactionError> {
fn source(&self) -> Option<&(dyn std::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 std::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 std::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](reth_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")]
Overdraft,
/// 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))
}
}