//! Ethereum transaction validator.

use super::constants::DEFAULT_MAX_TX_INPUT_BYTES;
use crate::{
    blobstore::{BlobStore, PooledBlobSidecar},
    error::{
        Eip4844PoolTransactionError, Eip7702PoolTransactionError, InvalidPoolTransactionError,
    },
    metrics::TxPoolValidationMetrics,
    traits::TransactionOrigin,
    validate::ValidTransaction,
    Address, EthBlobTransactionSidecar, EthPoolTransaction, LocalTransactionConfig,
    TransactionValidationOutcome, TransactionValidationTaskExecutor, TransactionValidator,
};

use alloy_consensus::{
    constants::{
        EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID,
        LEGACY_TX_TYPE_ID,
    },
    BlockHeader,
};
use alloy_eips::{
    eip1559::ETHEREUM_BLOCK_GAS_LIMIT_30M, eip4844::env_settings::EnvKzgSettings,
    eip7840::BlobParams, BlockId,
};
use alloy_primitives::U256;
use alloy_rlp::Encodable;
use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
use reth_evm::ConfigureEvm;
use reth_primitives_traits::{
    transaction::error::InvalidTransactionError, Account, BlockTy, GotExpected, HeaderTy,
    SealedBlock,
};
use reth_storage_api::{
    errors::ProviderError, AccountInfoReader, BlockReaderIdExt, BytecodeReader, StateProviderBox,
    StateProviderFactory,
};
use reth_tasks::Runtime;
use revm::context_interface::Cfg;
use std::{
    fmt,
    marker::PhantomData,
    sync::{
        atomic::{AtomicBool, AtomicU64, AtomicUsize},
        Arc,
    },
    time::{Instant, SystemTime},
};

/// Additional stateless validation function signature.
///
/// Receives the transaction origin and a reference to the transaction. Returns `Ok(())` if the
/// transaction passes or `Err` to reject it.
pub type StatelessValidationFn<T> =
    Arc<dyn Fn(TransactionOrigin, &T) -> Result<(), InvalidPoolTransactionError> + Send + Sync>;

/// Additional stateful validation function signature.
///
/// Receives the transaction origin, a reference to the transaction, and an account state reader.
/// Returns `Ok(())` if the transaction passes or `Err` to reject it.
pub type StatefulValidationFn<T> = Arc<
    dyn Fn(TransactionOrigin, &T, &dyn AccountInfoReader) -> Result<(), InvalidPoolTransactionError>
        + Send
        + Sync,
>;

/// A [`TransactionValidator`] implementation that validates ethereum transaction.
///
/// It supports all known ethereum transaction types:
/// - Legacy
/// - EIP-2718
/// - EIP-1559
/// - EIP-4844
/// - EIP-7702
///
/// And enforces additional constraints such as:
/// - Maximum transaction size
/// - Maximum gas limit
///
/// And adheres to the configured [`LocalTransactionConfig`].
pub struct EthTransactionValidator<Client, T, Evm> {
    /// This type fetches account info from the db
    client: Client,
    /// The chain ID transactions must use.
    chain_id: u64,
    /// Blobstore used for fetching re-injected blob transactions.
    blob_store: Box<dyn BlobStore>,
    /// tracks activated forks relevant for transaction validation
    fork_tracker: ForkTracker,
    /// Fork indicator whether we are using EIP-2718 type transactions.
    eip2718: bool,
    /// Fork indicator whether we are using EIP-1559 type transactions.
    eip1559: bool,
    /// Fork indicator whether we are using EIP-4844 blob transactions.
    eip4844: bool,
    /// Fork indicator whether we are using EIP-7702 type transactions.
    eip7702: bool,
    /// The current max gas limit
    block_gas_limit: AtomicU64,
    /// The current tx fee cap limit in wei locally submitted into the pool.
    tx_fee_cap: Option<u128>,
    /// Minimum priority fee to enforce for acceptance into the pool.
    minimum_priority_fee: Option<u128>,
    /// Stores the setup and parameters needed for validating KZG proofs.
    kzg_settings: EnvKzgSettings,
    /// How to handle [`TransactionOrigin::Local`](TransactionOrigin) transactions.
    local_transactions_config: LocalTransactionConfig,
    /// Maximum size in bytes a single transaction can have in order to be accepted into the pool.
    max_tx_input_bytes: usize,
    /// Maximum gas limit for individual transactions
    max_tx_gas_limit: Option<u64>,
    /// Disable balance checks during transaction validation
    disable_balance_check: bool,
    /// EVM configuration for fetching execution limits
    evm_config: Evm,
    /// Marker for the transaction type
    _marker: PhantomData<T>,
    /// Metrics for tsx pool validation
    validation_metrics: TxPoolValidationMetrics,
    /// Bitmap of custom transaction types that are allowed.
    other_tx_types: U256,
    /// Whether EIP-7594 blob sidecars are accepted.
    /// When false, EIP-7594 (v1) sidecars are always rejected and EIP-4844 (v0) sidecars
    /// are always accepted, regardless of Osaka fork activation.
    eip7594: bool,
    /// Optional additional stateless validation check applied at the end of
    /// [`validate_stateless`](Self::validate_stateless).
    additional_stateless_validation: Option<StatelessValidationFn<T>>,
    /// Optional additional stateful validation check applied at the end of
    /// [`validate_stateful`](Self::validate_stateful).
    additional_stateful_validation: Option<StatefulValidationFn<T>>,
}

impl<Client, Tx, Evm> fmt::Debug for EthTransactionValidator<Client, Tx, Evm> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("EthTransactionValidator")
            .field("fork_tracker", &self.fork_tracker)
            .field("eip2718", &self.eip2718)
            .field("eip1559", &self.eip1559)
            .field("eip4844", &self.eip4844)
            .field("eip7702", &self.eip7702)
            .field("block_gas_limit", &self.block_gas_limit)
            .field("tx_fee_cap", &self.tx_fee_cap)
            .field("minimum_priority_fee", &self.minimum_priority_fee)
            .field("max_tx_input_bytes", &self.max_tx_input_bytes)
            .field("max_tx_gas_limit", &self.max_tx_gas_limit)
            .field("disable_balance_check", &self.disable_balance_check)
            .field("eip7594", &self.eip7594)
            .field(
                "additional_stateless_validation",
                &self.additional_stateless_validation.as_ref().map(|_| "..."),
            )
            .field(
                "additional_stateful_validation",
                &self.additional_stateful_validation.as_ref().map(|_| "..."),
            )
            .finish()
    }
}

impl<Client, Tx, Evm> EthTransactionValidator<Client, Tx, Evm> {
    /// Returns the configured chain spec
    pub fn chain_spec(&self) -> Arc<Client::ChainSpec>
    where
        Client: ChainSpecProvider,
    {
        self.client().chain_spec()
    }

    /// Returns the configured chain id
    pub const fn chain_id(&self) -> u64 {
        self.chain_id
    }

    /// Returns the configured client
    pub const fn client(&self) -> &Client {
        &self.client
    }

    /// Returns the tracks activated forks relevant for transaction validation
    pub const fn fork_tracker(&self) -> &ForkTracker {
        &self.fork_tracker
    }

    /// Returns the EVM config used for transaction validation.
    pub const fn evm_config(&self) -> &Evm {
        &self.evm_config
    }

    /// Returns if there are EIP-2718 type transactions
    pub const fn eip2718(&self) -> bool {
        self.eip2718
    }

    /// Returns if there are EIP-1559 type transactions
    pub const fn eip1559(&self) -> bool {
        self.eip1559
    }

    /// Returns if there are EIP-4844 blob transactions
    pub const fn eip4844(&self) -> bool {
        self.eip4844
    }

    /// Returns if there are EIP-7702 type transactions
    pub const fn eip7702(&self) -> bool {
        self.eip7702
    }

    /// Returns the current tx fee cap limit in wei locally submitted into the pool
    pub const fn tx_fee_cap(&self) -> &Option<u128> {
        &self.tx_fee_cap
    }

    /// Returns the minimum priority fee to enforce for acceptance into the pool
    pub const fn minimum_priority_fee(&self) -> &Option<u128> {
        &self.minimum_priority_fee
    }

    /// Returns the setup and parameters needed for validating KZG proofs.
    pub const fn kzg_settings(&self) -> &EnvKzgSettings {
        &self.kzg_settings
    }

    /// Returns the config to handle [`TransactionOrigin::Local`](TransactionOrigin) transactions..
    pub const fn local_transactions_config(&self) -> &LocalTransactionConfig {
        &self.local_transactions_config
    }

    /// Returns the maximum size in bytes a single transaction can have in order to be accepted into
    /// the pool.
    pub const fn max_tx_input_bytes(&self) -> usize {
        self.max_tx_input_bytes
    }

    /// Returns whether balance checks are disabled for this validator.
    pub const fn disable_balance_check(&self) -> bool {
        self.disable_balance_check
    }

    /// Sets an additional stateless validation check that is applied at the end of
    /// [`validate_stateless`](Self::validate_stateless).
    ///
    /// The check receives the transaction origin and a reference to the transaction, and
    /// should return `Ok(())` if the transaction is valid or
    /// `Err(InvalidPoolTransactionError)` to reject it.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use reth_transaction_pool::{error::InvalidPoolTransactionError, TransactionOrigin};
    ///
    /// let mut validator = builder.build(blob_store);
    /// // Reject external transactions with input data exceeding 1KB
    /// validator.set_additional_stateless_validation(|origin, tx| {
    ///     if origin.is_external() && tx.input().len() > 1024 {
    ///         return Err(InvalidPoolTransactionError::OversizedData {
    ///             size: tx.input().len(),
    ///             limit: 1024,
    ///         });
    ///     }
    ///     Ok(())
    /// });
    /// ```
    pub fn set_additional_stateless_validation<F>(&mut self, f: F)
    where
        F: Fn(TransactionOrigin, &Tx) -> Result<(), InvalidPoolTransactionError>
            + Send
            + Sync
            + 'static,
    {
        self.additional_stateless_validation = Some(Arc::new(f));
    }

    /// Sets the additional stateless validation check from an already shared
    /// [`StatelessValidationFn`].
    ///
    /// This is useful when the same hook is shared across multiple validators, avoiding an extra
    /// allocation compared to
    /// [`set_additional_stateless_validation`](Self::set_additional_stateless_validation).
    pub fn set_additional_stateless_validation_fn(&mut self, f: StatelessValidationFn<Tx>) {
        self.additional_stateless_validation = Some(f);
    }

    /// Sets or clears the additional stateless validation check from an optional
    /// [`StatelessValidationFn`].
    ///
    /// Passing `None` removes any previously configured check.
    pub fn set_additional_stateless_validation_fn_opt(
        &mut self,
        f: Option<StatelessValidationFn<Tx>>,
    ) {
        self.additional_stateless_validation = f;
    }

    /// Sets an additional stateful validation check that is applied at the end of
    /// [`validate_stateful`](Self::validate_stateful).
    ///
    /// The check receives the transaction origin, a reference to the transaction, and the
    /// account state reader, and should return `Ok(())` if the transaction is valid or
    /// `Err(InvalidPoolTransactionError)` to reject it.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use reth_transaction_pool::{error::InvalidPoolTransactionError, TransactionOrigin};
    ///
    /// let mut validator = builder.build(blob_store);
    /// // Reject transactions from accounts with zero balance
    /// validator.set_additional_stateful_validation(|origin, tx, state| {
    ///     let account = state.basic_account(tx.sender_ref())?.unwrap_or_default();
    ///     if account.balance.is_zero() {
    ///         return Err(InvalidPoolTransactionError::Other(Box::new(
    ///             std::io::Error::new(std::io::ErrorKind::Other, "zero balance"),
    ///         )));
    ///     }
    ///     Ok(())
    /// });
    /// ```
    pub fn set_additional_stateful_validation<F>(&mut self, f: F)
    where
        F: Fn(
                TransactionOrigin,
                &Tx,
                &dyn AccountInfoReader,
            ) -> Result<(), InvalidPoolTransactionError>
            + Send
            + Sync
            + 'static,
    {
        self.additional_stateful_validation = Some(Arc::new(f));
    }

    /// Sets the additional stateful validation check from an already shared
    /// [`StatefulValidationFn`].
    ///
    /// This is useful when the same hook is shared across multiple validators, avoiding an extra
    /// allocation compared to
    /// [`set_additional_stateful_validation`](Self::set_additional_stateful_validation).
    pub fn set_additional_stateful_validation_fn(&mut self, f: StatefulValidationFn<Tx>) {
        self.additional_stateful_validation = Some(f);
    }

    /// Sets or clears the additional stateful validation check from an optional
    /// [`StatefulValidationFn`].
    ///
    /// Passing `None` removes any previously configured check.
    pub fn set_additional_stateful_validation_fn_opt(
        &mut self,
        f: Option<StatefulValidationFn<Tx>>,
    ) {
        self.additional_stateful_validation = f;
    }
}

impl<Client, Tx, Evm> EthTransactionValidator<Client, Tx, Evm>
where
    Client: ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks> + StateProviderFactory,
    Tx: EthPoolTransaction,
    Evm: ConfigureEvm,
{
    /// Returns the current max gas limit
    pub fn block_gas_limit(&self) -> u64 {
        self.max_gas_limit()
    }

    /// Validates a single transaction.
    ///
    /// See also [`TransactionValidator::validate_transaction`]
    pub fn validate_one(
        &self,
        origin: TransactionOrigin,
        transaction: Tx,
    ) -> TransactionValidationOutcome<Tx> {
        let mut state: Option<StateProviderBox> = None;
        self.validate_one_with_provider(origin, transaction, &mut state, || self.client.latest())
    }

    /// Validates a single transaction with the provided state provider.
    ///
    /// This allows reusing the same provider across multiple transaction validations,
    /// which can improve performance when validating many transactions.
    ///
    /// If `state` is `None`, a new state provider will be created.
    pub fn validate_one_with_state(
        &self,
        origin: TransactionOrigin,
        transaction: Tx,
        state: &mut Option<Box<dyn AccountInfoReader + Send>>,
    ) -> TransactionValidationOutcome<Tx> {
        self.validate_one_with_provider(origin, transaction, state, || {
            self.client.latest().map(|state| Box::new(state) as Box<dyn AccountInfoReader + Send>)
        })
    }

    /// Validates a single transaction using an optional cached state provider.
    /// If no provider is passed, a new one will be created. This allows reusing
    /// the same provider across multiple txs.
    fn validate_one_with_provider<P, F>(
        &self,
        origin: TransactionOrigin,
        transaction: Tx,
        maybe_state: &mut Option<P>,
        state_provider: F,
    ) -> TransactionValidationOutcome<Tx>
    where
        P: AccountInfoReader,
        F: FnOnce() -> Result<P, ProviderError>,
    {
        match self.validate_stateless(origin, &transaction) {
            Ok(()) => {
                // stateless checks passed, pass transaction down stateful validation pipeline
                // If we don't have a state provider yet, fetch the latest state
                if maybe_state.is_none() {
                    match state_provider() {
                        Ok(new_state) => {
                            *maybe_state = Some(new_state);
                        }
                        Err(err) => {
                            return TransactionValidationOutcome::Error(
                                *transaction.hash(),
                                Box::new(err),
                            )
                        }
                    }
                }

                let state = maybe_state.as_ref().expect("provider is set");

                self.validate_stateful(origin, transaction, state)
            }
            Err(err) => TransactionValidationOutcome::Invalid(transaction, err),
        }
    }

    /// Validates a single transaction against the given state provider, performing both
    /// [stateless](Self::validate_stateless) and [stateful](Self::validate_stateful) checks.
    pub fn validate_one_with_state_provider(
        &self,
        origin: TransactionOrigin,
        transaction: Tx,
        state: impl AccountInfoReader,
    ) -> TransactionValidationOutcome<Tx> {
        if let Err(err) = self.validate_stateless(origin, &transaction) {
            return TransactionValidationOutcome::Invalid(transaction, err);
        }
        self.validate_stateful(origin, transaction, state)
    }

    /// Validates a single transaction without requiring any state access (stateless checks only).
    ///
    /// Checks tx type support, nonce bounds, size limits, gas limits, fee constraints, chain ID,
    /// intrinsic gas, and blob tx pre-checks.
    pub fn validate_stateless(
        &self,
        origin: TransactionOrigin,
        transaction: &Tx,
    ) -> Result<(), InvalidPoolTransactionError> {
        // Checks for tx_type
        match transaction.ty() {
            // Accept only legacy transactions until EIP-2718/2930 activates
            EIP2930_TX_TYPE_ID if !self.eip2718 => {
                return Err(InvalidTransactionError::Eip2930Disabled.into())
            }
            // Reject dynamic fee transactions until EIP-1559 activates.
            EIP1559_TX_TYPE_ID if !self.eip1559 => {
                return Err(InvalidTransactionError::Eip1559Disabled.into())
            }
            // Reject blob transactions.
            EIP4844_TX_TYPE_ID if !self.eip4844 => {
                return Err(InvalidTransactionError::Eip4844Disabled.into())
            }
            // Reject EIP-7702 transactions.
            EIP7702_TX_TYPE_ID if !self.eip7702 => {
                return Err(InvalidTransactionError::Eip7702Disabled.into())
            }
            // Accept known transaction types when their respective fork is active
            LEGACY_TX_TYPE_ID | EIP2930_TX_TYPE_ID | EIP1559_TX_TYPE_ID | EIP4844_TX_TYPE_ID |
            EIP7702_TX_TYPE_ID => {}

            ty if !self.other_tx_types.bit(ty as usize) => {
                return Err(InvalidTransactionError::TxTypeNotSupported.into())
            }

            _ => {}
        };

        // Reject transactions with a nonce equal to U64::max according to EIP-2681
        let tx_nonce = transaction.nonce();
        if tx_nonce == u64::MAX {
            return Err(InvalidPoolTransactionError::Eip2681)
        }

        // Reject transactions over defined size to prevent DOS attacks
        if transaction.is_eip4844() {
            // Since blob transactions are pulled instead of pushed, and only the consensus data is
            // kept in memory while the sidecar is cached on disk, there is no critical limit that
            // should be enforced. Still, enforcing some cap on the dynamic transaction data. blob
            // txs also must be executable right away when they enter the pool.
            let tx_size = transaction.input().len().saturating_add(
                transaction
                    .access_list()
                    .map(|access_list| access_list.length())
                    .unwrap_or_default(),
            );
            if tx_size > self.max_tx_input_bytes {
                return Err(InvalidPoolTransactionError::OversizedData {
                    size: tx_size,
                    limit: self.max_tx_input_bytes,
                })
            }
        } else {
            // ensure the size of the non-blob transaction
            let tx_size = transaction.encoded_length();
            if tx_size > self.max_tx_input_bytes {
                return Err(InvalidPoolTransactionError::OversizedData {
                    size: tx_size,
                    limit: self.max_tx_input_bytes,
                })
            }
        }

        // Check whether the init code size has been exceeded.
        if self.fork_tracker.is_shanghai_activated() {
            let max_initcode_size =
                self.fork_tracker.max_initcode_size.load(std::sync::atomic::Ordering::Relaxed);
            transaction.ensure_max_init_code_size(max_initcode_size)?;
        }

        // Checks for gas limit
        let transaction_gas_limit = transaction.gas_limit();
        let block_gas_limit = self.max_gas_limit();
        if transaction_gas_limit > block_gas_limit {
            return Err(InvalidPoolTransactionError::ExceedsGasLimit(
                transaction_gas_limit,
                block_gas_limit,
            ))
        }

        // Check individual transaction gas limit if configured
        if let Some(max_tx_gas_limit) = self.max_tx_gas_limit &&
            transaction_gas_limit > max_tx_gas_limit
        {
            return Err(InvalidPoolTransactionError::MaxTxGasLimitExceeded(
                transaction_gas_limit,
                max_tx_gas_limit,
            ))
        }

        // Ensure max_priority_fee_per_gas (if EIP1559) is less than max_fee_per_gas if any.
        if transaction.max_priority_fee_per_gas() > Some(transaction.max_fee_per_gas()) {
            return Err(InvalidTransactionError::TipAboveFeeCap.into())
        }

        // determine whether the transaction should be treated as local
        let is_local = self.local_transactions_config.is_local(origin, transaction.sender_ref());

        // Ensure max possible transaction fee doesn't exceed configured transaction fee cap.
        // Only for transactions locally submitted for acceptance into the pool.
        if is_local {
            match self.tx_fee_cap {
                Some(0) | None => {} // Skip if cap is 0 or None
                Some(tx_fee_cap_wei) => {
                    let max_tx_fee_wei = transaction.cost().saturating_sub(transaction.value());
                    if max_tx_fee_wei > tx_fee_cap_wei {
                        return Err(InvalidPoolTransactionError::ExceedsFeeCap {
                            max_tx_fee_wei: max_tx_fee_wei.saturating_to(),
                            tx_fee_cap_wei,
                        })
                    }
                }
            }
        }

        // Drop non-local transactions with a fee lower than the configured fee for acceptance into
        // the pool.
        if !is_local &&
            transaction.is_dynamic_fee() &&
            transaction.max_priority_fee_per_gas() < self.minimum_priority_fee
        {
            return Err(InvalidPoolTransactionError::PriorityFeeBelowMinimum {
                minimum_priority_fee: self
                    .minimum_priority_fee
                    .expect("minimum priority fee is expected inside if statement"),
            })
        }

        // Checks for chainid
        if let Some(chain_id) = transaction.chain_id() &&
            chain_id != self.chain_id()
        {
            return Err(InvalidTransactionError::ChainIdMismatch.into())
        }

        if transaction.is_eip7702() {
            // Prague fork is required for 7702 txs
            if !self.fork_tracker.is_prague_activated() {
                return Err(InvalidTransactionError::TxTypeNotSupported.into())
            }

            if transaction.authorization_list().is_none_or(|l| l.is_empty()) {
                return Err(Eip7702PoolTransactionError::MissingEip7702AuthorizationList.into())
            }
        }

        ensure_intrinsic_gas(transaction, &self.fork_tracker)?;

        // light blob tx pre-checks
        if transaction.is_eip4844() {
            // Cancun fork is required for blob txs
            if !self.fork_tracker.is_cancun_activated() {
                return Err(InvalidTransactionError::TxTypeNotSupported.into())
            }

            let blob_count = transaction.blob_count().unwrap_or(0);
            if blob_count == 0 {
                // no blobs
                return Err(InvalidPoolTransactionError::Eip4844(
                    Eip4844PoolTransactionError::NoEip4844Blobs,
                ))
            }

            let max_blob_count = self.fork_tracker.max_blob_count();
            if blob_count > max_blob_count {
                return Err(InvalidPoolTransactionError::Eip4844(
                    Eip4844PoolTransactionError::TooManyEip4844Blobs {
                        have: blob_count,
                        permitted: max_blob_count,
                    },
                ))
            }
        }

        // Transaction gas limit validation (EIP-7825 for Osaka+)
        let tx_gas_limit_cap =
            self.fork_tracker.tx_gas_limit_cap.load(std::sync::atomic::Ordering::Relaxed);
        if tx_gas_limit_cap > 0 && transaction.gas_limit() > tx_gas_limit_cap {
            return Err(InvalidTransactionError::GasLimitTooHigh.into())
        }

        // Run additional stateless validation if configured
        if let Some(check) = &self.additional_stateless_validation {
            check(origin, transaction)?;
        }

        Ok(())
    }

    /// Validates a single transaction against the given state (stateful checks only).
    ///
    /// Checks sender account balance, nonce, bytecode, and validates blob sidecars. The
    /// transaction must have already passed [`validate_stateless`](Self::validate_stateless).
    pub fn validate_stateful<P>(
        &self,
        origin: TransactionOrigin,
        mut transaction: Tx,
        state: P,
    ) -> TransactionValidationOutcome<Tx>
    where
        P: AccountInfoReader,
    {
        // Use provider to get account info
        let account = match state.basic_account(transaction.sender_ref()) {
            Ok(account) => account.unwrap_or_default(),
            Err(err) => {
                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err))
            }
        };

        // check for bytecode
        match self.validate_sender_bytecode(&transaction, &account, &state) {
            Err(outcome) => return outcome,
            Ok(Err(err)) => return TransactionValidationOutcome::Invalid(transaction, err),
            _ => {}
        };

        // Checks for nonce
        if transaction.requires_nonce_check() &&
            let Err(err) = self.validate_sender_nonce(&transaction, &account)
        {
            return TransactionValidationOutcome::Invalid(transaction, err)
        }

        // checks for max cost not exceedng account_balance
        if let Err(err) = self.validate_sender_balance(&transaction, &account) {
            return TransactionValidationOutcome::Invalid(transaction, err)
        }

        // heavy blob tx validation
        let maybe_blob_sidecar = match self.validate_eip4844(&mut transaction) {
            Err(err) => return TransactionValidationOutcome::Invalid(transaction, err),
            Ok(sidecar) => sidecar,
        };

        // Run additional stateful validation if configured
        if let Some(check) = &self.additional_stateful_validation &&
            let Err(err) = check(origin, &transaction, &state)
        {
            return TransactionValidationOutcome::Invalid(transaction, err)
        }

        let authorities = self.recover_authorities(&transaction);
        // Return the valid transaction
        TransactionValidationOutcome::Valid {
            balance: account.balance,
            state_nonce: account.nonce,
            bytecode_hash: account.bytecode_hash,
            transaction: ValidTransaction::new(transaction, maybe_blob_sidecar),
            // by this point assume all external transactions should be propagated
            propagate: match origin {
                TransactionOrigin::External => true,
                TransactionOrigin::Local => {
                    self.local_transactions_config.propagate_local_transactions
                }
                TransactionOrigin::Private => false,
            },
            authorities,
        }
    }

    /// Validates that the sender’s account has valid or no bytecode.
    pub fn validate_sender_bytecode(
        &self,
        transaction: &Tx,
        sender: &Account,
        state: impl BytecodeReader,
    ) -> Result<Result<(), InvalidPoolTransactionError>, TransactionValidationOutcome<Tx>> {
        // Unless Prague is active, the signer account shouldn't have bytecode.
        //
        // If Prague is active, only EIP-7702 bytecode is allowed for the sender.
        //
        // Any other case means that the account is not an EOA, and should not be able to send
        // transactions.
        if let Some(code_hash) = &sender.bytecode_hash {
            let is_eip7702 = if self.fork_tracker.is_prague_activated() {
                match state.bytecode_by_hash(code_hash) {
                    Ok(bytecode) => bytecode.unwrap_or_default().is_eip7702(),
                    Err(err) => {
                        return Err(TransactionValidationOutcome::Error(
                            *transaction.hash(),
                            Box::new(err),
                        ))
                    }
                }
            } else {
                false
            };

            if !is_eip7702 {
                return Ok(Err(InvalidTransactionError::SignerAccountHasBytecode.into()))
            }
        }
        Ok(Ok(()))
    }

    /// Checks if the transaction nonce is valid.
    pub fn validate_sender_nonce(
        &self,
        transaction: &Tx,
        sender: &Account,
    ) -> Result<(), InvalidPoolTransactionError> {
        let tx_nonce = transaction.nonce();

        if tx_nonce < sender.nonce {
            return Err(InvalidTransactionError::NonceNotConsistent {
                tx: tx_nonce,
                state: sender.nonce,
            }
            .into())
        }
        Ok(())
    }

    /// Ensures the sender has sufficient account balance.
    pub fn validate_sender_balance(
        &self,
        transaction: &Tx,
        sender: &Account,
    ) -> Result<(), InvalidPoolTransactionError> {
        let cost = transaction.cost();

        if !self.disable_balance_check && cost > &sender.balance {
            let expected = *cost;
            return Err(InvalidTransactionError::InsufficientFunds(
                GotExpected { got: sender.balance, expected }.into(),
            )
            .into())
        }
        Ok(())
    }

    /// Validates EIP-4844 blob sidecar data and returns the extracted sidecar, if any.
    pub fn validate_eip4844(
        &self,
        transaction: &mut Tx,
    ) -> Result<Option<PooledBlobSidecar>, InvalidPoolTransactionError> {
        let mut maybe_blob_sidecar = None;

        // heavy blob tx validation
        if transaction.is_eip4844() {
            // extract the blob from the transaction
            match transaction.take_blob() {
                EthBlobTransactionSidecar::None => {
                    // this should not happen
                    return Err(InvalidTransactionError::TxTypeNotSupported.into())
                }
                EthBlobTransactionSidecar::Missing => {
                    // This can happen for re-injected blob transactions (on re-org), since the blob
                    // is stripped from the transaction and not included in a block.
                    // check if the blob is in the store, if it's included we previously validated
                    // it and inserted it
                    if self.blob_store.contains(*transaction.hash()).is_ok_and(|c| c) {
                        // validated transaction is already in the store
                    } else {
                        return Err(InvalidPoolTransactionError::Eip4844(
                            Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
                        ))
                    }
                }
                EthBlobTransactionSidecar::Present(sidecar) => {
                    let now = Instant::now();

                    // EIP-7594 sidecar version handling
                    if self.eip7594 {
                        // Standard Ethereum behavior
                        if self.fork_tracker.is_osaka_activated() {
                            if sidecar.is_eip4844() {
                                return Err(InvalidPoolTransactionError::Eip4844(
                                    Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka,
                                ))
                            }
                        } else if sidecar.is_eip7594() && !self.allow_7594_sidecars() {
                            return Err(InvalidPoolTransactionError::Eip4844(
                                Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka,
                            ))
                        }
                    } else {
                        // EIP-7594 disabled: always reject v1 sidecars, accept v0
                        if sidecar.is_eip7594() {
                            return Err(InvalidPoolTransactionError::Eip4844(
                                Eip4844PoolTransactionError::Eip7594SidecarDisallowed,
                            ))
                        }
                    }

                    // validate the blob
                    if let Err(err) = transaction.validate_blob(&sidecar, self.kzg_settings.get()) {
                        return Err(InvalidPoolTransactionError::Eip4844(
                            Eip4844PoolTransactionError::InvalidEip4844Blob(err),
                        ))
                    }
                    // Record the duration of successful blob validation as histogram
                    self.validation_metrics.blob_validation_duration.record(now.elapsed());
                    // store the extracted blob
                    maybe_blob_sidecar = Some(sidecar);
                }
            }
        }
        Ok(maybe_blob_sidecar)
    }

    /// Returns the recovered authorities for the given transaction
    fn recover_authorities(&self, transaction: &Tx) -> std::option::Option<Vec<Address>> {
        transaction
            .authorization_list()
            .map(|auths| auths.iter().flat_map(|auth| auth.recover_authority()).collect::<Vec<_>>())
    }

    /// Validates all given transactions.
    fn validate_batch(
        &self,
        transactions: impl IntoIterator<Item = (TransactionOrigin, Tx)>,
    ) -> Vec<TransactionValidationOutcome<Tx>> {
        let mut provider: Option<StateProviderBox> = None;
        transactions
            .into_iter()
            .map(|(origin, tx)| {
                self.validate_one_with_provider(origin, tx, &mut provider, || self.client.latest())
            })
            .collect()
    }

    /// Validates all given transactions with origin.
    fn validate_batch_with_origin(
        &self,
        origin: TransactionOrigin,
        transactions: impl IntoIterator<Item = Tx> + Send,
    ) -> Vec<TransactionValidationOutcome<Tx>> {
        let mut provider: Option<StateProviderBox> = None;
        transactions
            .into_iter()
            .map(|tx| {
                self.validate_one_with_provider(origin, tx, &mut provider, || self.client.latest())
            })
            .collect()
    }

    fn on_new_head_block(&self, new_tip_block: &HeaderTy<Evm::Primitives>) {
        // update all forks
        if self.chain_spec().is_shanghai_active_at_timestamp(new_tip_block.timestamp()) {
            self.fork_tracker.shanghai.store(true, std::sync::atomic::Ordering::Relaxed);
        }

        if self.chain_spec().is_cancun_active_at_timestamp(new_tip_block.timestamp()) {
            self.fork_tracker.cancun.store(true, std::sync::atomic::Ordering::Relaxed);
        }

        if self.chain_spec().is_prague_active_at_timestamp(new_tip_block.timestamp()) {
            self.fork_tracker.prague.store(true, std::sync::atomic::Ordering::Relaxed);
        }

        if self.chain_spec().is_osaka_active_at_timestamp(new_tip_block.timestamp()) {
            self.fork_tracker.osaka.store(true, std::sync::atomic::Ordering::Relaxed);
        }

        if self.chain_spec().is_amsterdam_active_at_timestamp(new_tip_block.timestamp()) {
            self.fork_tracker.amsterdam.store(true, std::sync::atomic::Ordering::Relaxed);
        }

        self.fork_tracker
            .tip_timestamp
            .store(new_tip_block.timestamp(), std::sync::atomic::Ordering::Relaxed);

        if let Some(blob_params) =
            self.chain_spec().blob_params_at_timestamp(new_tip_block.timestamp())
        {
            self.fork_tracker
                .max_blob_count
                .store(blob_params.max_blobs_per_tx, std::sync::atomic::Ordering::Relaxed);
        }

        self.block_gas_limit.store(new_tip_block.gas_limit(), std::sync::atomic::Ordering::Relaxed);

        // Get EVM limits from evm_config.evm_env()
        let evm_env = self
            .evm_config
            .evm_env(new_tip_block)
            .expect("evm_env should not fail for executed block");

        self.fork_tracker
            .max_initcode_size
            .store(evm_env.cfg_env.max_initcode_size(), std::sync::atomic::Ordering::Relaxed);
        // EIP-8037: When state gas is enabled, `tx.gas` can exceed the per-tx gas limit cap
        // because the cap only applies to regular gas (state gas uses a reservoir).
        // Store 0 to disable the txpool-level check.
        let tx_gas_limit_cap = if evm_env.cfg_env.is_amsterdam_eip8037_enabled() {
            0
        } else {
            evm_env.cfg_env.tx_gas_limit_cap()
        };
        self.fork_tracker
            .tx_gas_limit_cap
            .store(tx_gas_limit_cap, std::sync::atomic::Ordering::Relaxed);
    }

    fn max_gas_limit(&self) -> u64 {
        self.block_gas_limit.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Returns whether EIP-7594 sidecars are allowed
    fn allow_7594_sidecars(&self) -> bool {
        let tip_timestamp = self.fork_tracker.tip_timestamp();

        // If next block is Osaka, allow 7594 sidecars
        if self.chain_spec().is_osaka_active_at_timestamp(tip_timestamp.saturating_add(12)) {
            true
        } else if self.chain_spec().is_osaka_active_at_timestamp(tip_timestamp.saturating_add(24)) {
            let current_timestamp =
                SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();

            // Allow after 4 seconds into last non-Osaka slot
            current_timestamp >= tip_timestamp.saturating_add(4)
        } else {
            false
        }
    }
}

impl<Client, Tx, Evm> TransactionValidator for EthTransactionValidator<Client, Tx, Evm>
where
    Client: ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks> + StateProviderFactory,
    Tx: EthPoolTransaction,
    Evm: ConfigureEvm,
{
    type Transaction = Tx;
    type Block = BlockTy<Evm::Primitives>;

    async fn validate_transaction(
        &self,
        origin: TransactionOrigin,
        transaction: Self::Transaction,
    ) -> TransactionValidationOutcome<Self::Transaction> {
        self.validate_one(origin, transaction)
    }

    async fn validate_transactions(
        &self,
        transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction), IntoIter: Send>
            + Send,
    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
        self.validate_batch(transactions)
    }

    async fn validate_transactions_with_origin(
        &self,
        origin: TransactionOrigin,
        transactions: impl IntoIterator<Item = Self::Transaction, IntoIter: Send> + Send,
    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
        self.validate_batch_with_origin(origin, transactions)
    }

    fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
        Self::on_new_head_block(self, new_tip_block.header())
    }
}

/// A builder for [`EthTransactionValidator`] and [`TransactionValidationTaskExecutor`]
#[derive(Debug)]
pub struct EthTransactionValidatorBuilder<Client, Evm> {
    client: Client,
    /// The chain ID transactions must use.
    chain_id: u64,
    /// The EVM configuration to use for validation.
    evm_config: Evm,
    /// Fork indicator whether we are in the Shanghai stage.
    shanghai: bool,
    /// Fork indicator whether we are in the Cancun hardfork.
    cancun: bool,
    /// Fork indicator whether we are in the Prague hardfork.
    prague: bool,
    /// Fork indicator whether we are in the Osaka hardfork.
    osaka: bool,
    /// Fork indicator whether we are in the Amsterdam hardfork.
    amsterdam: bool,
    /// Timestamp of the tip block.
    tip_timestamp: u64,
    /// Max blob count at the block's timestamp.
    max_blob_count: u64,
    /// Whether using EIP-2718 type transactions is allowed
    eip2718: bool,
    /// Whether using EIP-1559 type transactions is allowed
    eip1559: bool,
    /// Whether using EIP-4844 type transactions is allowed
    eip4844: bool,
    /// Whether using EIP-7702 type transactions is allowed
    eip7702: bool,
    /// The current max gas limit
    block_gas_limit: AtomicU64,
    /// The current tx fee cap limit in wei locally submitted into the pool.
    tx_fee_cap: Option<u128>,
    /// Minimum priority fee to enforce for acceptance into the pool.
    minimum_priority_fee: Option<u128>,
    /// Determines how many additional tasks to spawn
    ///
    /// Default is 1
    additional_tasks: usize,

    /// Stores the setup and parameters needed for validating KZG proofs.
    kzg_settings: EnvKzgSettings,
    /// How to handle [`TransactionOrigin::Local`](TransactionOrigin) transactions.
    local_transactions_config: LocalTransactionConfig,
    /// Max size in bytes of a single transaction allowed
    max_tx_input_bytes: usize,
    /// Maximum gas limit for individual transactions
    max_tx_gas_limit: Option<u64>,
    /// Disable balance checks during transaction validation
    disable_balance_check: bool,
    /// Bitmap of custom transaction types that are allowed.
    other_tx_types: U256,
    /// Cached max initcode size from EVM config
    max_initcode_size: usize,
    /// Cached transaction gas limit cap from EVM config (0 = no cap)
    tx_gas_limit_cap: u64,
    /// Whether EIP-7594 blob sidecars are accepted.
    /// When false, EIP-7594 (v1) sidecars are always rejected and EIP-4844 (v0) sidecars
    /// are always accepted, regardless of Osaka fork activation.
    eip7594: bool,
}

impl<Client, Evm> EthTransactionValidatorBuilder<Client, Evm> {
    /// Creates a new builder for the given client and EVM config
    ///
    /// By default this assumes the network is on the `Prague` hardfork and the following
    /// transactions are allowed:
    ///  - Legacy
    ///  - EIP-2718
    ///  - EIP-1559
    ///  - EIP-4844
    ///  - EIP-7702
    pub fn new(client: Client, evm_config: Evm) -> Self
    where
        Client: ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks>
            + BlockReaderIdExt<Header = HeaderTy<Evm::Primitives>>,
        Evm: ConfigureEvm,
    {
        let chain_spec = client.chain_spec();
        let tip = client
            .header_by_id(BlockId::latest())
            .expect("failed to fetch latest header")
            .expect("latest header is not found");
        let evm_env =
            evm_config.evm_env(&tip).expect("evm_env should not fail for existing blocks");

        Self {
            block_gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M.into(),
            client,
            chain_id: chain_spec.chain().id(),
            evm_config,
            minimum_priority_fee: None,
            additional_tasks: 1,
            kzg_settings: EnvKzgSettings::Default,
            local_transactions_config: Default::default(),
            max_tx_input_bytes: DEFAULT_MAX_TX_INPUT_BYTES,
            tx_fee_cap: Some(1e18 as u128),
            max_tx_gas_limit: None,
            // by default all transaction types are allowed
            eip2718: true,
            eip1559: true,
            eip4844: true,
            eip7702: true,

            shanghai: chain_spec.is_shanghai_active_at_timestamp(tip.timestamp()),
            cancun: chain_spec.is_cancun_active_at_timestamp(tip.timestamp()),
            prague: chain_spec.is_prague_active_at_timestamp(tip.timestamp()),
            osaka: chain_spec.is_osaka_active_at_timestamp(tip.timestamp()),
            amsterdam: chain_spec.is_amsterdam_active_at_timestamp(tip.timestamp()),

            tip_timestamp: tip.timestamp(),

            max_blob_count: chain_spec
                .blob_params_at_timestamp(tip.timestamp())
                .unwrap_or_else(BlobParams::prague)
                .max_blobs_per_tx,

            // balance checks are enabled by default
            disable_balance_check: false,

            // no custom transaction types by default
            other_tx_types: U256::ZERO,

            // EIP-8037: When state gas is enabled, tx.gas can exceed the per-tx cap
            tx_gas_limit_cap: if evm_env.cfg_env.is_amsterdam_eip8037_enabled() {
                0
            } else {
                evm_env.cfg_env.tx_gas_limit_cap()
            },
            max_initcode_size: evm_env.cfg_env.max_initcode_size(),

            // EIP-7594 sidecars are accepted by default (standard Ethereum behavior)
            eip7594: true,
        }
    }

    /// Disables the Cancun fork.
    pub const fn no_cancun(self) -> Self {
        self.set_cancun(false)
    }

    /// Whether to allow exemptions for local transaction exemptions.
    pub fn with_local_transactions_config(
        mut self,
        local_transactions_config: LocalTransactionConfig,
    ) -> Self {
        self.local_transactions_config = local_transactions_config;
        self
    }

    /// Set the Cancun fork.
    pub const fn set_cancun(mut self, cancun: bool) -> Self {
        self.cancun = cancun;
        self
    }

    /// Disables the Shanghai fork.
    pub const fn no_shanghai(self) -> Self {
        self.set_shanghai(false)
    }

    /// Set the Shanghai fork.
    pub const fn set_shanghai(mut self, shanghai: bool) -> Self {
        self.shanghai = shanghai;
        self
    }

    /// Disables the Prague fork.
    pub const fn no_prague(self) -> Self {
        self.set_prague(false)
    }

    /// Set the Prague fork.
    pub const fn set_prague(mut self, prague: bool) -> Self {
        self.prague = prague;
        self
    }

    /// Disables the Osaka fork.
    pub const fn no_osaka(self) -> Self {
        self.set_osaka(false)
    }

    /// Set the Osaka fork.
    pub const fn set_osaka(mut self, osaka: bool) -> Self {
        self.osaka = osaka;
        self
    }

    /// Disables the Amsterdam fork.
    pub const fn no_amsterdam(self) -> Self {
        self.set_amsterdam(false)
    }

    /// Set the Amsterdam fork.
    pub const fn set_amsterdam(mut self, amsterdam: bool) -> Self {
        self.amsterdam = amsterdam;
        self
    }

    /// Disables the support for EIP-2718 transactions.
    pub const fn no_eip2718(self) -> Self {
        self.set_eip2718(false)
    }

    /// Set the support for EIP-2718 transactions.
    pub const fn set_eip2718(mut self, eip2718: bool) -> Self {
        self.eip2718 = eip2718;
        self
    }

    /// Disables the support for EIP-1559 transactions.
    pub const fn no_eip1559(self) -> Self {
        self.set_eip1559(false)
    }

    /// Set the support for EIP-1559 transactions.
    pub const fn set_eip1559(mut self, eip1559: bool) -> Self {
        self.eip1559 = eip1559;
        self
    }

    /// Disables the support for EIP-4844 transactions.
    pub const fn no_eip4844(self) -> Self {
        self.set_eip4844(false)
    }

    /// Set the support for EIP-4844 transactions.
    pub const fn set_eip4844(mut self, eip4844: bool) -> Self {
        self.eip4844 = eip4844;
        self
    }

    /// Disables the support for EIP-7702 transactions.
    pub const fn no_eip7702(self) -> Self {
        self.set_eip7702(false)
    }

    /// Set the support for EIP-7702 transactions.
    pub const fn set_eip7702(mut self, eip7702: bool) -> Self {
        self.eip7702 = eip7702;
        self
    }

    /// Disables EIP-7594 blob sidecar support.
    ///
    /// When disabled, EIP-7594 (v1) blob sidecars are always rejected and EIP-4844 (v0)
    /// sidecars are always accepted, regardless of Osaka fork activation.
    ///
    /// Use this for chains that do not adopt EIP-7594 (`PeerDAS`).
    pub const fn no_eip7594(self) -> Self {
        self.set_eip7594(false)
    }

    /// Set EIP-7594 blob sidecar support.
    ///
    /// When true (default), standard Ethereum behavior applies: v0 sidecars before Osaka,
    /// v1 sidecars after Osaka. When false, v1 sidecars are always rejected.
    pub const fn set_eip7594(mut self, eip7594: bool) -> Self {
        self.eip7594 = eip7594;
        self
    }

    /// Sets the [`EnvKzgSettings`] to use for validating KZG proofs.
    pub fn kzg_settings(mut self, kzg_settings: EnvKzgSettings) -> Self {
        self.kzg_settings = kzg_settings;
        self
    }

    /// Sets a minimum priority fee that's enforced for acceptance into the pool.
    pub const fn with_minimum_priority_fee(mut self, minimum_priority_fee: Option<u128>) -> Self {
        self.minimum_priority_fee = minimum_priority_fee;
        self
    }

    /// Sets the number of additional tasks to spawn.
    pub const fn with_additional_tasks(mut self, additional_tasks: usize) -> Self {
        self.additional_tasks = additional_tasks;
        self
    }

    /// Sets a max size in bytes of a single transaction allowed into the pool
    pub const fn with_max_tx_input_bytes(mut self, max_tx_input_bytes: usize) -> Self {
        self.max_tx_input_bytes = max_tx_input_bytes;
        self
    }

    /// Sets the block gas limit
    ///
    /// Transactions with a gas limit greater than this will be rejected.
    pub fn set_block_gas_limit(self, block_gas_limit: u64) -> Self {
        self.block_gas_limit.store(block_gas_limit, std::sync::atomic::Ordering::Relaxed);
        self
    }

    /// Sets the block gas limit
    ///
    /// Transactions with a gas limit greater than this will be rejected.
    pub const fn set_tx_fee_cap(mut self, tx_fee_cap: u128) -> Self {
        self.tx_fee_cap = Some(tx_fee_cap);
        self
    }

    /// Sets the maximum gas limit for individual transactions
    pub const fn with_max_tx_gas_limit(mut self, max_tx_gas_limit: Option<u64>) -> Self {
        self.max_tx_gas_limit = max_tx_gas_limit;
        self
    }

    /// Disables balance checks during transaction validation
    pub const fn disable_balance_check(mut self) -> Self {
        self.disable_balance_check = true;
        self
    }

    /// Adds a custom transaction type to the validator.
    pub const fn with_custom_tx_type(mut self, tx_type: u8) -> Self {
        self.other_tx_types.set_bit(tx_type as usize, true);
        self
    }

    /// Builds a the [`EthTransactionValidator`] without spawning validator tasks.
    pub fn build<Tx, S>(self, blob_store: S) -> EthTransactionValidator<Client, Tx, Evm>
    where
        S: BlobStore,
    {
        let Self {
            client,
            chain_id,
            evm_config,
            shanghai,
            cancun,
            prague,
            osaka,
            amsterdam,
            tip_timestamp,
            eip2718,
            eip1559,
            eip4844,
            eip7702,
            block_gas_limit,
            tx_fee_cap,
            minimum_priority_fee,
            kzg_settings,
            local_transactions_config,
            max_tx_input_bytes,
            max_tx_gas_limit,
            disable_balance_check,
            max_blob_count,
            additional_tasks: _,
            other_tx_types,
            max_initcode_size,
            tx_gas_limit_cap,
            eip7594,
        } = self;

        let fork_tracker = ForkTracker {
            shanghai: AtomicBool::new(shanghai),
            cancun: AtomicBool::new(cancun),
            prague: AtomicBool::new(prague),
            osaka: AtomicBool::new(osaka),
            amsterdam: AtomicBool::new(amsterdam),
            tip_timestamp: AtomicU64::new(tip_timestamp),
            max_blob_count: AtomicU64::new(max_blob_count),
            max_initcode_size: AtomicUsize::new(max_initcode_size),
            tx_gas_limit_cap: AtomicU64::new(tx_gas_limit_cap),
        };

        EthTransactionValidator {
            client,
            chain_id,
            eip2718,
            eip1559,
            fork_tracker,
            eip4844,
            eip7702,
            block_gas_limit,
            tx_fee_cap,
            minimum_priority_fee,
            blob_store: Box::new(blob_store),
            kzg_settings,
            local_transactions_config,
            max_tx_input_bytes,
            max_tx_gas_limit,
            disable_balance_check,
            evm_config,
            _marker: Default::default(),
            validation_metrics: TxPoolValidationMetrics::default(),
            other_tx_types,
            eip7594,
            additional_stateless_validation: None,
            additional_stateful_validation: None,
        }
    }

    /// Builds a [`EthTransactionValidator`] and spawns validation tasks via the
    /// [`TransactionValidationTaskExecutor`]
    ///
    /// The validator will spawn `additional_tasks` additional tasks for validation.
    ///
    /// By default this will spawn 1 additional task.
    pub fn build_with_tasks<Tx, S>(
        self,
        tasks: Runtime,
        blob_store: S,
    ) -> TransactionValidationTaskExecutor<EthTransactionValidator<Client, Tx, Evm>>
    where
        S: BlobStore,
    {
        let additional_tasks = self.additional_tasks;
        let validator = self.build::<Tx, S>(blob_store);
        TransactionValidationTaskExecutor::spawn(validator, &tasks, additional_tasks)
    }
}

/// Keeps track of whether certain forks are activated
#[derive(Debug)]
pub struct ForkTracker {
    /// Tracks if shanghai is activated at the block's timestamp.
    pub shanghai: AtomicBool,
    /// Tracks if cancun is activated at the block's timestamp.
    pub cancun: AtomicBool,
    /// Tracks if prague is activated at the block's timestamp.
    pub prague: AtomicBool,
    /// Tracks if osaka is activated at the block's timestamp.
    pub osaka: AtomicBool,
    /// Tracks if amsterdam is activated at the block's timestamp.
    pub amsterdam: AtomicBool,
    /// Tracks max blob count per transaction at the block's timestamp.
    pub max_blob_count: AtomicU64,
    /// Tracks the timestamp of the tip block.
    pub tip_timestamp: AtomicU64,
    /// Cached max initcode size from EVM config
    pub max_initcode_size: AtomicUsize,
    /// Cached transaction gas limit cap from EVM config (0 = no cap)
    pub tx_gas_limit_cap: AtomicU64,
}

impl ForkTracker {
    /// Returns `true` if Shanghai fork is activated.
    pub fn is_shanghai_activated(&self) -> bool {
        self.shanghai.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Returns `true` if Cancun fork is activated.
    pub fn is_cancun_activated(&self) -> bool {
        self.cancun.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Returns `true` if Prague fork is activated.
    pub fn is_prague_activated(&self) -> bool {
        self.prague.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Returns `true` if Osaka fork is activated.
    pub fn is_osaka_activated(&self) -> bool {
        self.osaka.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Returns `true` if Amsterdam fork is activated.
    pub fn is_amsterdam_activated(&self) -> bool {
        self.amsterdam.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Returns the timestamp of the tip block.
    pub fn tip_timestamp(&self) -> u64 {
        self.tip_timestamp.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Returns the max allowed blob count per transaction.
    pub fn max_blob_count(&self) -> u64 {
        self.max_blob_count.load(std::sync::atomic::Ordering::Relaxed)
    }
}

/// Ensures that gas limit of the transaction exceeds the intrinsic gas of the transaction.
///
/// Caution: This only checks past the Merge hardfork.
pub fn ensure_intrinsic_gas<T: EthPoolTransaction>(
    transaction: &T,
    fork_tracker: &ForkTracker,
) -> Result<(), InvalidPoolTransactionError> {
    use revm::primitives::hardfork::SpecId;
    let spec_id = if fork_tracker.is_amsterdam_activated() {
        SpecId::AMSTERDAM
    } else if fork_tracker.is_prague_activated() {
        SpecId::PRAGUE
    } else if fork_tracker.is_shanghai_activated() {
        SpecId::SHANGHAI
    } else {
        SpecId::MERGE
    };

    // EIP-2780 replaces the flat intrinsic base cost with a decomposed one that depends on
    // `tx.to` and `tx.value`.
    let eip2780 = fork_tracker.is_amsterdam_activated().then(|| {
        revm::context_interface::cfg::gas_params::Eip2780TxInfo {
            value: transaction.value(),
            // Self-transfer: a `Call` whose recipient is the sender itself.
            is_self_transfer: transaction.kind().to() == Some(&transaction.sender()),
        }
    });

    let gas = revm::interpreter::gas::calculate_initial_tx_gas(
        spec_id,
        transaction.input(),
        transaction.is_create(),
        transaction.access_list().map(|l| l.len()).unwrap_or_default() as u64,
        transaction
            .access_list()
            .map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
            .unwrap_or_default() as u64,
        transaction.authorization_list().map(|l| l.len()).unwrap_or_default() as u64,
        eip2780,
    );

    let gas_limit = transaction.gas_limit();
    if gas_limit < gas.initial_total_gas() || gas_limit < gas.floor_gas {
        Err(InvalidPoolTransactionError::IntrinsicGasTooLow)
    } else {
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        blobstore::InMemoryBlobStore, error::PoolErrorKind, test_utils::TransactionBuilder,
        traits::PoolTransaction, CoinbaseTipOrdering, EthPooledTransaction, Pool, TransactionPool,
    };
    use alloy_consensus::Transaction;
    use alloy_eips::{
        eip2718::{Decodable2718, Encodable2718},
        eip2930::{AccessList, AccessListItem},
    };
    use alloy_primitives::{hex, Address, B256, U256};
    use reth_ethereum_primitives::PooledTransactionVariant;
    use reth_evm_ethereum::EthEvmConfig;
    use reth_primitives_traits::SignedTransaction;
    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
    use revm::primitives::eip3860::MAX_INITCODE_SIZE;

    fn test_evm_config() -> EthEvmConfig {
        EthEvmConfig::mainnet()
    }

    fn get_transaction() -> EthPooledTransaction {
        let raw = " ";

        let data = hex::decode(raw).unwrap();
        let tx = PooledTransactionVariant::decode_2718(&mut data.as_ref()).unwrap();

        EthPooledTransaction::from_pooled(tx.try_into_recovered().unwrap())
    }

    fn eip1559_tx(
        to: Address,
        sender: Address,
        value: u64,
        gas_limit: u64,
    ) -> EthPooledTransaction {
        let tx = alloy_consensus::TxEip1559 {
            chain_id: 1,
            nonce: 0,
            gas_limit,
            max_fee_per_gas: 1,
            max_priority_fee_per_gas: 0,
            to: to.into(),
            value: U256::from(value),
            ..Default::default()
        };
        let signed = reth_ethereum_primitives::TransactionSigned::new_unhashed(
            tx.into(),
            alloy_primitives::Signature::test_signature(),
        );
        EthPooledTransaction::new(
            alloy_consensus::transaction::Recovered::new_unchecked(signed, sender),
            200,
        )
    }

    /// EIP-2780 replaces the flat 21k intrinsic base with a decomposed one: 12k base, plus a cold
    /// account access for `tx.to` and a transfer charge when `tx.value` is non-zero, with a
    /// carve-out for self-transfers.
    #[test]
    fn intrinsic_gas_eip2780() {
        let sender = Address::repeat_byte(1);
        let recipient = Address::repeat_byte(2);

        let amsterdam = || ForkTracker {
            shanghai: true.into(),
            cancun: true.into(),
            prague: true.into(),
            osaka: true.into(),
            amsterdam: true.into(),
            tip_timestamp: 0.into(),
            max_blob_count: 0.into(),
            max_initcode_size: AtomicUsize::new(MAX_INITCODE_SIZE),
            tx_gas_limit_cap: AtomicU64::new(0),
        };
        let pre_amsterdam = || ForkTracker { amsterdam: false.into(), ..amsterdam() };

        // Self-transfer: base cost only (12k), where pre-Amsterdam it pays the flat 21k.
        let self_transfer = eip1559_tx(sender, sender, 1, 15_000);
        assert!(ensure_intrinsic_gas(&self_transfer, &amsterdam()).is_ok());
        assert!(ensure_intrinsic_gas(&self_transfer, &pre_amsterdam()).is_err());

        // Zero-value call to another account: base + cold account access (15k).
        let zero_value = eip1559_tx(recipient, sender, 0, 15_000);
        assert!(ensure_intrinsic_gas(&zero_value, &amsterdam()).is_ok());
        assert!(
            ensure_intrinsic_gas(&eip1559_tx(recipient, sender, 0, 14_999), &amsterdam()).is_err()
        );

        // Value transfer to another account: base + cold access + transfer log + value cost (21k).
        assert!(
            ensure_intrinsic_gas(&eip1559_tx(recipient, sender, 1, 15_000), &amsterdam()).is_err()
        );
        assert!(
            ensure_intrinsic_gas(&eip1559_tx(recipient, sender, 1, 21_000), &amsterdam()).is_ok()
        );
    }

    // <https://github.com/paradigmxyz/reth/issues/5178>
    #[tokio::test]
    async fn validate_transaction() {
        let transaction = get_transaction();
        let mut fork_tracker = ForkTracker {
            shanghai: false.into(),
            cancun: false.into(),
            prague: false.into(),
            osaka: false.into(),
            amsterdam: false.into(),
            tip_timestamp: 0.into(),
            max_blob_count: 0.into(),
            max_initcode_size: AtomicUsize::new(MAX_INITCODE_SIZE),
            tx_gas_limit_cap: AtomicU64::new(0),
        };

        let res = ensure_intrinsic_gas(&transaction, &fork_tracker);
        assert!(res.is_ok());

        fork_tracker.shanghai = true.into();
        let res = ensure_intrinsic_gas(&transaction, &fork_tracker);
        assert!(res.is_ok());

        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );
        let blob_store = InMemoryBlobStore::default();
        let validator = EthTransactionValidatorBuilder::new(provider, test_evm_config())
            .build(blob_store.clone());

        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());

        assert!(outcome.is_valid());

        let pool =
            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());

        let res = pool.add_external_transaction(transaction.clone()).await;
        assert!(res.is_ok());
        let tx = pool.get(transaction.hash());
        assert!(tx.is_some());
    }

    #[test]
    fn validates_configured_chain_id() {
        let provider = MockEthProvider::default().with_genesis_block();
        let validator = EthTransactionValidatorBuilder::new(provider, test_evm_config())
            .build(InMemoryBlobStore::default());
        let transaction = |chain_id| {
            EthPooledTransaction::try_from_consensus(
                TransactionBuilder::default()
                    .chain_id(chain_id)
                    .gas_limit(21_000)
                    .to(Address::ZERO)
                    .into_eip1559()
                    .try_into_recovered()
                    .unwrap(),
            )
            .unwrap()
        };

        assert!(validator
            .validate_stateless(TransactionOrigin::External, &transaction(validator.chain_id()))
            .is_ok());
        assert!(matches!(
            validator.validate_stateless(
                TransactionOrigin::External,
                &transaction(validator.chain_id() + 1)
            ),
            Err(InvalidPoolTransactionError::Consensus(InvalidTransactionError::ChainIdMismatch))
        ));
    }

    // <https://github.com/paradigmxyz/reth/issues/8550>
    #[tokio::test]
    async fn invalid_on_gas_limit_too_high() {
        let transaction = get_transaction();

        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );

        let blob_store = InMemoryBlobStore::default();
        let validator = EthTransactionValidatorBuilder::new(provider, test_evm_config())
            .set_block_gas_limit(1_000_000) // tx gas limit is 1_015_288
            .build(blob_store.clone());

        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());

        assert!(outcome.is_invalid());

        let pool =
            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());

        let res = pool.add_external_transaction(transaction.clone()).await;
        assert!(res.is_err());
        assert!(matches!(
            res.unwrap_err().kind,
            PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::ExceedsGasLimit(
                1_015_288, 1_000_000
            ))
        ));
        let tx = pool.get(transaction.hash());
        assert!(tx.is_none());
    }

    #[tokio::test]
    async fn invalid_on_fee_cap_exceeded() {
        let transaction = get_transaction();
        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );

        let blob_store = InMemoryBlobStore::default();
        let validator = EthTransactionValidatorBuilder::new(provider, test_evm_config())
            .set_tx_fee_cap(100) // 100 wei cap
            .build(blob_store.clone());

        let outcome = validator.validate_one(TransactionOrigin::Local, transaction.clone());
        assert!(outcome.is_invalid());

        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
            assert!(matches!(
                err,
                InvalidPoolTransactionError::ExceedsFeeCap { max_tx_fee_wei, tx_fee_cap_wei }
                if (max_tx_fee_wei > tx_fee_cap_wei)
            ));
        }

        let pool =
            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
        let res = pool.add_transaction(TransactionOrigin::Local, transaction.clone()).await;
        assert!(res.is_err());
        assert!(matches!(
            res.unwrap_err().kind,
            PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::ExceedsFeeCap { .. })
        ));
        let tx = pool.get(transaction.hash());
        assert!(tx.is_none());
    }

    #[tokio::test]
    async fn valid_on_zero_fee_cap() {
        let transaction = get_transaction();
        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );

        let blob_store = InMemoryBlobStore::default();
        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
            .set_tx_fee_cap(0) // no cap
            .build(blob_store);

        let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
        assert!(outcome.is_valid());
    }

    #[tokio::test]
    async fn valid_on_normal_fee_cap() {
        let transaction = get_transaction();
        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );

        let blob_store = InMemoryBlobStore::default();
        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
            .set_tx_fee_cap(2e18 as u128) // 2 ETH cap
            .build(blob_store);

        let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
        assert!(outcome.is_valid());
    }

    #[tokio::test]
    async fn invalid_on_max_tx_gas_limit_exceeded() {
        let transaction = get_transaction();
        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );

        let blob_store = InMemoryBlobStore::default();
        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
            .with_max_tx_gas_limit(Some(500_000)) // Set limit lower than transaction gas limit (1_015_288)
            .build(blob_store.clone());

        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
        assert!(outcome.is_invalid());

        let pool =
            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());

        let res = pool.add_external_transaction(transaction.clone()).await;
        assert!(res.is_err());
        assert!(matches!(
            res.unwrap_err().kind,
            PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::MaxTxGasLimitExceeded(
                1_015_288, 500_000
            ))
        ));
        let tx = pool.get(transaction.hash());
        assert!(tx.is_none());
    }

    #[tokio::test]
    async fn valid_on_max_tx_gas_limit_disabled() {
        let transaction = get_transaction();
        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );

        let blob_store = InMemoryBlobStore::default();
        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
            .with_max_tx_gas_limit(None) // disabled
            .build(blob_store);

        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
        assert!(outcome.is_valid());
    }

    #[tokio::test]
    async fn valid_on_max_tx_gas_limit_within_limit() {
        let transaction = get_transaction();
        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );

        let blob_store = InMemoryBlobStore::default();
        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
            .with_max_tx_gas_limit(Some(2_000_000)) // Set limit higher than transaction gas limit (1_015_288)
            .build(blob_store);

        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
        assert!(outcome.is_valid());
    }

    // Helper function to set up common test infrastructure for priority fee tests
    fn setup_priority_fee_test() -> (EthPooledTransaction, MockEthProvider) {
        let transaction = get_transaction();
        let provider = MockEthProvider::default().with_genesis_block();
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), U256::MAX),
        );
        (transaction, provider)
    }

    // Helper function to create a validator with minimum priority fee
    fn create_validator_with_minimum_fee(
        provider: MockEthProvider,
        minimum_priority_fee: Option<u128>,
        local_config: Option<LocalTransactionConfig>,
    ) -> EthTransactionValidator<MockEthProvider, EthPooledTransaction, EthEvmConfig> {
        let blob_store = InMemoryBlobStore::default();
        let mut builder = EthTransactionValidatorBuilder::new(provider, test_evm_config())
            .with_minimum_priority_fee(minimum_priority_fee);

        if let Some(config) = local_config {
            builder = builder.with_local_transactions_config(config);
        }

        builder.build(blob_store)
    }

    #[tokio::test]
    async fn invalid_on_priority_fee_lower_than_configured_minimum() {
        let (transaction, provider) = setup_priority_fee_test();

        // Verify the test transaction is a dynamic fee transaction
        assert!(transaction.is_dynamic_fee());

        // Set minimum priority fee to be double the transaction's priority fee
        let minimum_priority_fee =
            transaction.max_priority_fee_per_gas().expect("priority fee is expected") * 2;

        let validator =
            create_validator_with_minimum_fee(provider, Some(minimum_priority_fee), None);

        // External transaction should be rejected due to low priority fee
        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
        assert!(outcome.is_invalid());

        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
            assert!(matches!(
                err,
                InvalidPoolTransactionError::PriorityFeeBelowMinimum { minimum_priority_fee: min_fee }
                if min_fee == minimum_priority_fee
            ));
        }

        // Test pool integration
        let blob_store = InMemoryBlobStore::default();
        let pool =
            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());

        let res = pool.add_external_transaction(transaction.clone()).await;
        assert!(res.is_err());
        assert!(matches!(
            res.unwrap_err().kind,
            PoolErrorKind::InvalidTransaction(
                InvalidPoolTransactionError::PriorityFeeBelowMinimum { .. }
            )
        ));
        let tx = pool.get(transaction.hash());
        assert!(tx.is_none());

        // Local transactions should still be accepted regardless of minimum priority fee
        let (_, local_provider) = setup_priority_fee_test();
        let validator_local =
            create_validator_with_minimum_fee(local_provider, Some(minimum_priority_fee), None);

        let local_outcome = validator_local.validate_one(TransactionOrigin::Local, transaction);
        assert!(local_outcome.is_valid());
    }

    #[tokio::test]
    async fn valid_on_priority_fee_equal_to_minimum() {
        let (transaction, provider) = setup_priority_fee_test();

        // Set minimum priority fee equal to transaction's priority fee
        let tx_priority_fee =
            transaction.max_priority_fee_per_gas().expect("priority fee is expected");
        let validator = create_validator_with_minimum_fee(provider, Some(tx_priority_fee), None);

        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
        assert!(outcome.is_valid());
    }

    #[tokio::test]
    async fn valid_on_priority_fee_above_minimum() {
        let (transaction, provider) = setup_priority_fee_test();

        // Set minimum priority fee below transaction's priority fee
        let tx_priority_fee =
            transaction.max_priority_fee_per_gas().expect("priority fee is expected");
        let minimum_priority_fee = tx_priority_fee / 2; // Half of transaction's priority fee

        let validator =
            create_validator_with_minimum_fee(provider, Some(minimum_priority_fee), None);

        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
        assert!(outcome.is_valid());
    }

    #[tokio::test]
    async fn valid_on_minimum_priority_fee_disabled() {
        let (transaction, provider) = setup_priority_fee_test();

        // No minimum priority fee set (default is None)
        let validator = create_validator_with_minimum_fee(provider, None, None);

        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
        assert!(outcome.is_valid());
    }

    #[tokio::test]
    async fn priority_fee_validation_applies_to_private_transactions() {
        let (transaction, provider) = setup_priority_fee_test();

        // Set minimum priority fee to be double the transaction's priority fee
        let minimum_priority_fee =
            transaction.max_priority_fee_per_gas().expect("priority fee is expected") * 2;

        let validator =
            create_validator_with_minimum_fee(provider, Some(minimum_priority_fee), None);

        // Private transactions are also subject to minimum priority fee validation
        // because they are not considered "local" by default unless specifically configured
        let outcome = validator.validate_one(TransactionOrigin::Private, transaction);
        assert!(outcome.is_invalid());

        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
            assert!(matches!(
                err,
                InvalidPoolTransactionError::PriorityFeeBelowMinimum { minimum_priority_fee: min_fee }
                if min_fee == minimum_priority_fee
            ));
        }
    }

    #[tokio::test]
    async fn valid_on_local_config_exempts_private_transactions() {
        let (transaction, provider) = setup_priority_fee_test();

        // Set minimum priority fee to be double the transaction's priority fee
        let minimum_priority_fee =
            transaction.max_priority_fee_per_gas().expect("priority fee is expected") * 2;

        // Configure local transactions to include all private transactions
        let local_config =
            LocalTransactionConfig { propagate_local_transactions: true, ..Default::default() };

        let validator = create_validator_with_minimum_fee(
            provider,
            Some(minimum_priority_fee),
            Some(local_config),
        );

        // With appropriate local config, the behavior depends on the local transaction logic
        // This test documents the current behavior - private transactions are still validated
        // unless the sender is specifically whitelisted in local_transactions_config
        let outcome = validator.validate_one(TransactionOrigin::Private, transaction);
        assert!(outcome.is_invalid()); // Still invalid because sender not in whitelist
    }

    #[test]
    fn reject_oversized_tx() {
        let mut transaction = get_transaction();
        transaction.encoded_length = DEFAULT_MAX_TX_INPUT_BYTES + 1;
        let provider = MockEthProvider::default().with_genesis_block();

        // No minimum priority fee set (default is None)
        let validator = create_validator_with_minimum_fee(provider, None, None);

        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
        let invalid = outcome.as_invalid().unwrap();
        assert!(invalid.is_oversized());
    }

    #[test]
    fn reject_blob_tx_with_oversized_access_list() {
        let max_tx_input_bytes = 512;
        let provider = MockEthProvider::default().with_genesis_block();
        let validator = EthTransactionValidatorBuilder::new(provider, test_evm_config())
            .with_max_tx_input_bytes(max_tx_input_bytes)
            .build(InMemoryBlobStore::default());

        let blob_tx_with_access_list = |storage_keys: usize| {
            let access_list = AccessList(vec![AccessListItem {
                address: Address::random(),
                storage_keys: (0..storage_keys).map(|_| B256::random()).collect(),
            }]);
            let tx = TransactionBuilder::default()
                .access_list(access_list)
                .into_eip4844()
                .try_into_recovered()
                .unwrap();
            let encoded_length = tx.encode_2718_len();
            EthPooledTransaction::new(tx, encoded_length)
        };

        let is_oversized = |tx: &EthPooledTransaction| {
            matches!(
                validator.validate_stateless(TransactionOrigin::External, tx),
                Err(InvalidPoolTransactionError::OversizedData { .. })
            )
        };

        let small = blob_tx_with_access_list(1);
        assert!(!is_oversized(&small));

        let large = blob_tx_with_access_list(64);
        assert!(large.input().is_empty());
        assert!(is_oversized(&large));
    }

    #[tokio::test]
    async fn valid_with_disabled_balance_check() {
        let transaction = get_transaction();
        let provider = MockEthProvider::default().with_genesis_block();

        // Set account with 0 balance
        provider.add_account(
            transaction.sender(),
            ExtendedAccount::new(transaction.nonce(), alloy_primitives::U256::ZERO),
        );

        // Validate with balance check enabled
        let validator =
            EthTransactionValidatorBuilder::new(provider.clone(), EthEvmConfig::mainnet())
                .build(InMemoryBlobStore::default());

        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
        let expected_cost = *transaction.cost();
        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
            assert!(matches!(
                err,
                InvalidPoolTransactionError::Consensus(InvalidTransactionError::InsufficientFunds(ref funds_err))
                if funds_err.got == alloy_primitives::U256::ZERO && funds_err.expected == expected_cost
            ));
        } else {
            panic!("Expected Invalid outcome with InsufficientFunds error");
        }

        // Validate with balance check disabled
        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
            .disable_balance_check()
            .build(InMemoryBlobStore::default());

        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
        assert!(outcome.is_valid()); // Should be valid because balance check is disabled
    }
}
