Skip to main content

reth_transaction_pool/validate/
eth.rs

1//! Ethereum transaction validator.
2
3use super::constants::DEFAULT_MAX_TX_INPUT_BYTES;
4use crate::{
5    blobstore::BlobStore,
6    error::{
7        Eip4844PoolTransactionError, Eip7702PoolTransactionError, InvalidPoolTransactionError,
8    },
9    metrics::TxPoolValidationMetrics,
10    traits::TransactionOrigin,
11    validate::ValidTransaction,
12    Address, BlobTransactionSidecarVariant, EthBlobTransactionSidecar, EthPoolTransaction,
13    LocalTransactionConfig, TransactionValidationOutcome, TransactionValidationTaskExecutor,
14    TransactionValidator,
15};
16
17use alloy_consensus::{
18    constants::{
19        EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID,
20        LEGACY_TX_TYPE_ID,
21    },
22    BlockHeader,
23};
24use alloy_eips::{
25    eip1559::ETHEREUM_BLOCK_GAS_LIMIT_30M, eip4844::env_settings::EnvKzgSettings,
26    eip7840::BlobParams, BlockId,
27};
28use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
29use reth_evm::ConfigureEvm;
30use reth_primitives_traits::{
31    transaction::error::InvalidTransactionError, Account, BlockTy, GotExpected, HeaderTy,
32    SealedBlock,
33};
34use reth_storage_api::{AccountInfoReader, BlockReaderIdExt, BytecodeReader, StateProviderFactory};
35use reth_tasks::Runtime;
36use revm::context_interface::Cfg;
37use revm_primitives::U256;
38use std::{
39    fmt,
40    marker::PhantomData,
41    sync::{
42        atomic::{AtomicBool, AtomicU64, AtomicUsize},
43        Arc,
44    },
45    time::{Instant, SystemTime},
46};
47
48/// Additional stateless validation function signature.
49///
50/// Receives the transaction origin and a reference to the transaction. Returns `Ok(())` if the
51/// transaction passes or `Err` to reject it.
52pub type StatelessValidationFn<T> =
53    Arc<dyn Fn(TransactionOrigin, &T) -> Result<(), InvalidPoolTransactionError> + Send + Sync>;
54
55/// Additional stateful validation function signature.
56///
57/// Receives the transaction origin, a reference to the transaction, and an account state reader.
58/// Returns `Ok(())` if the transaction passes or `Err` to reject it.
59pub type StatefulValidationFn<T> = Arc<
60    dyn Fn(TransactionOrigin, &T, &dyn AccountInfoReader) -> Result<(), InvalidPoolTransactionError>
61        + Send
62        + Sync,
63>;
64
65/// A [`TransactionValidator`] implementation that validates ethereum transaction.
66///
67/// It supports all known ethereum transaction types:
68/// - Legacy
69/// - EIP-2718
70/// - EIP-1559
71/// - EIP-4844
72/// - EIP-7702
73///
74/// And enforces additional constraints such as:
75/// - Maximum transaction size
76/// - Maximum gas limit
77///
78/// And adheres to the configured [`LocalTransactionConfig`].
79pub struct EthTransactionValidator<Client, T, Evm> {
80    /// This type fetches account info from the db
81    client: Client,
82    /// Blobstore used for fetching re-injected blob transactions.
83    blob_store: Box<dyn BlobStore>,
84    /// tracks activated forks relevant for transaction validation
85    fork_tracker: ForkTracker,
86    /// Fork indicator whether we are using EIP-2718 type transactions.
87    eip2718: bool,
88    /// Fork indicator whether we are using EIP-1559 type transactions.
89    eip1559: bool,
90    /// Fork indicator whether we are using EIP-4844 blob transactions.
91    eip4844: bool,
92    /// Fork indicator whether we are using EIP-7702 type transactions.
93    eip7702: bool,
94    /// The current max gas limit
95    block_gas_limit: AtomicU64,
96    /// The current tx fee cap limit in wei locally submitted into the pool.
97    tx_fee_cap: Option<u128>,
98    /// Minimum priority fee to enforce for acceptance into the pool.
99    minimum_priority_fee: Option<u128>,
100    /// Stores the setup and parameters needed for validating KZG proofs.
101    kzg_settings: EnvKzgSettings,
102    /// How to handle [`TransactionOrigin::Local`](TransactionOrigin) transactions.
103    local_transactions_config: LocalTransactionConfig,
104    /// Maximum size in bytes a single transaction can have in order to be accepted into the pool.
105    max_tx_input_bytes: usize,
106    /// Maximum gas limit for individual transactions
107    max_tx_gas_limit: Option<u64>,
108    /// Disable balance checks during transaction validation
109    disable_balance_check: bool,
110    /// EVM configuration for fetching execution limits
111    evm_config: Evm,
112    /// Marker for the transaction type
113    _marker: PhantomData<T>,
114    /// Metrics for tsx pool validation
115    validation_metrics: TxPoolValidationMetrics,
116    /// Bitmap of custom transaction types that are allowed.
117    other_tx_types: U256,
118    /// Whether EIP-7594 blob sidecars are accepted.
119    /// When false, EIP-7594 (v1) sidecars are always rejected and EIP-4844 (v0) sidecars
120    /// are always accepted, regardless of Osaka fork activation.
121    eip7594: bool,
122    /// Optional additional stateless validation check applied at the end of
123    /// [`validate_stateless`](Self::validate_stateless).
124    additional_stateless_validation: Option<StatelessValidationFn<T>>,
125    /// Optional additional stateful validation check applied at the end of
126    /// [`validate_stateful`](Self::validate_stateful).
127    additional_stateful_validation: Option<StatefulValidationFn<T>>,
128}
129
130impl<Client, Tx, Evm> fmt::Debug for EthTransactionValidator<Client, Tx, Evm> {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.debug_struct("EthTransactionValidator")
133            .field("fork_tracker", &self.fork_tracker)
134            .field("eip2718", &self.eip2718)
135            .field("eip1559", &self.eip1559)
136            .field("eip4844", &self.eip4844)
137            .field("eip7702", &self.eip7702)
138            .field("block_gas_limit", &self.block_gas_limit)
139            .field("tx_fee_cap", &self.tx_fee_cap)
140            .field("minimum_priority_fee", &self.minimum_priority_fee)
141            .field("max_tx_input_bytes", &self.max_tx_input_bytes)
142            .field("max_tx_gas_limit", &self.max_tx_gas_limit)
143            .field("disable_balance_check", &self.disable_balance_check)
144            .field("eip7594", &self.eip7594)
145            .field(
146                "additional_stateless_validation",
147                &self.additional_stateless_validation.as_ref().map(|_| "..."),
148            )
149            .field(
150                "additional_stateful_validation",
151                &self.additional_stateful_validation.as_ref().map(|_| "..."),
152            )
153            .finish()
154    }
155}
156
157impl<Client, Tx, Evm> EthTransactionValidator<Client, Tx, Evm> {
158    /// Returns the configured chain spec
159    pub fn chain_spec(&self) -> Arc<Client::ChainSpec>
160    where
161        Client: ChainSpecProvider,
162    {
163        self.client().chain_spec()
164    }
165
166    /// Returns the configured chain id
167    pub fn chain_id(&self) -> u64
168    where
169        Client: ChainSpecProvider,
170    {
171        self.client().chain_spec().chain().id()
172    }
173
174    /// Returns the configured client
175    pub const fn client(&self) -> &Client {
176        &self.client
177    }
178
179    /// Returns the tracks activated forks relevant for transaction validation
180    pub const fn fork_tracker(&self) -> &ForkTracker {
181        &self.fork_tracker
182    }
183
184    /// Returns the EVM config used for transaction validation.
185    pub const fn evm_config(&self) -> &Evm {
186        &self.evm_config
187    }
188
189    /// Returns if there are EIP-2718 type transactions
190    pub const fn eip2718(&self) -> bool {
191        self.eip2718
192    }
193
194    /// Returns if there are EIP-1559 type transactions
195    pub const fn eip1559(&self) -> bool {
196        self.eip1559
197    }
198
199    /// Returns if there are EIP-4844 blob transactions
200    pub const fn eip4844(&self) -> bool {
201        self.eip4844
202    }
203
204    /// Returns if there are EIP-7702 type transactions
205    pub const fn eip7702(&self) -> bool {
206        self.eip7702
207    }
208
209    /// Returns the current tx fee cap limit in wei locally submitted into the pool
210    pub const fn tx_fee_cap(&self) -> &Option<u128> {
211        &self.tx_fee_cap
212    }
213
214    /// Returns the minimum priority fee to enforce for acceptance into the pool
215    pub const fn minimum_priority_fee(&self) -> &Option<u128> {
216        &self.minimum_priority_fee
217    }
218
219    /// Returns the setup and parameters needed for validating KZG proofs.
220    pub const fn kzg_settings(&self) -> &EnvKzgSettings {
221        &self.kzg_settings
222    }
223
224    /// Returns the config to handle [`TransactionOrigin::Local`](TransactionOrigin) transactions..
225    pub const fn local_transactions_config(&self) -> &LocalTransactionConfig {
226        &self.local_transactions_config
227    }
228
229    /// Returns the maximum size in bytes a single transaction can have in order to be accepted into
230    /// the pool.
231    pub const fn max_tx_input_bytes(&self) -> usize {
232        self.max_tx_input_bytes
233    }
234
235    /// Returns whether balance checks are disabled for this validator.
236    pub const fn disable_balance_check(&self) -> bool {
237        self.disable_balance_check
238    }
239
240    /// Sets an additional stateless validation check that is applied at the end of
241    /// [`validate_stateless`](Self::validate_stateless).
242    ///
243    /// The check receives the transaction origin and a reference to the transaction, and
244    /// should return `Ok(())` if the transaction is valid or
245    /// `Err(InvalidPoolTransactionError)` to reject it.
246    ///
247    /// # Example
248    ///
249    /// ```ignore
250    /// use reth_transaction_pool::{error::InvalidPoolTransactionError, TransactionOrigin};
251    ///
252    /// let mut validator = builder.build(blob_store);
253    /// // Reject external transactions with input data exceeding 1KB
254    /// validator.set_additional_stateless_validation(|origin, tx| {
255    ///     if origin.is_external() && tx.input().len() > 1024 {
256    ///         return Err(InvalidPoolTransactionError::OversizedData {
257    ///             size: tx.input().len(),
258    ///             limit: 1024,
259    ///         });
260    ///     }
261    ///     Ok(())
262    /// });
263    /// ```
264    pub fn set_additional_stateless_validation<F>(&mut self, f: F)
265    where
266        F: Fn(TransactionOrigin, &Tx) -> Result<(), InvalidPoolTransactionError>
267            + Send
268            + Sync
269            + 'static,
270    {
271        self.additional_stateless_validation = Some(Arc::new(f));
272    }
273
274    /// Sets the additional stateless validation check from an already shared
275    /// [`StatelessValidationFn`].
276    ///
277    /// This is useful when the same hook is shared across multiple validators, avoiding an extra
278    /// allocation compared to
279    /// [`set_additional_stateless_validation`](Self::set_additional_stateless_validation).
280    pub fn set_additional_stateless_validation_fn(&mut self, f: StatelessValidationFn<Tx>) {
281        self.additional_stateless_validation = Some(f);
282    }
283
284    /// Sets or clears the additional stateless validation check from an optional
285    /// [`StatelessValidationFn`].
286    ///
287    /// Passing `None` removes any previously configured check.
288    pub fn set_additional_stateless_validation_fn_opt(
289        &mut self,
290        f: Option<StatelessValidationFn<Tx>>,
291    ) {
292        self.additional_stateless_validation = f;
293    }
294
295    /// Sets an additional stateful validation check that is applied at the end of
296    /// [`validate_stateful`](Self::validate_stateful).
297    ///
298    /// The check receives the transaction origin, a reference to the transaction, and the
299    /// account state reader, and should return `Ok(())` if the transaction is valid or
300    /// `Err(InvalidPoolTransactionError)` to reject it.
301    ///
302    /// # Example
303    ///
304    /// ```ignore
305    /// use reth_transaction_pool::{error::InvalidPoolTransactionError, TransactionOrigin};
306    ///
307    /// let mut validator = builder.build(blob_store);
308    /// // Reject transactions from accounts with zero balance
309    /// validator.set_additional_stateful_validation(|origin, tx, state| {
310    ///     let account = state.basic_account(tx.sender_ref())?.unwrap_or_default();
311    ///     if account.balance.is_zero() {
312    ///         return Err(InvalidPoolTransactionError::Other(Box::new(
313    ///             std::io::Error::new(std::io::ErrorKind::Other, "zero balance"),
314    ///         )));
315    ///     }
316    ///     Ok(())
317    /// });
318    /// ```
319    pub fn set_additional_stateful_validation<F>(&mut self, f: F)
320    where
321        F: Fn(
322                TransactionOrigin,
323                &Tx,
324                &dyn AccountInfoReader,
325            ) -> Result<(), InvalidPoolTransactionError>
326            + Send
327            + Sync
328            + 'static,
329    {
330        self.additional_stateful_validation = Some(Arc::new(f));
331    }
332
333    /// Sets the additional stateful validation check from an already shared
334    /// [`StatefulValidationFn`].
335    ///
336    /// This is useful when the same hook is shared across multiple validators, avoiding an extra
337    /// allocation compared to
338    /// [`set_additional_stateful_validation`](Self::set_additional_stateful_validation).
339    pub fn set_additional_stateful_validation_fn(&mut self, f: StatefulValidationFn<Tx>) {
340        self.additional_stateful_validation = Some(f);
341    }
342
343    /// Sets or clears the additional stateful validation check from an optional
344    /// [`StatefulValidationFn`].
345    ///
346    /// Passing `None` removes any previously configured check.
347    pub fn set_additional_stateful_validation_fn_opt(
348        &mut self,
349        f: Option<StatefulValidationFn<Tx>>,
350    ) {
351        self.additional_stateful_validation = f;
352    }
353}
354
355impl<Client, Tx, Evm> EthTransactionValidator<Client, Tx, Evm>
356where
357    Client: ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks> + StateProviderFactory,
358    Tx: EthPoolTransaction,
359    Evm: ConfigureEvm,
360{
361    /// Returns the current max gas limit
362    pub fn block_gas_limit(&self) -> u64 {
363        self.max_gas_limit()
364    }
365
366    /// Validates a single transaction.
367    ///
368    /// See also [`TransactionValidator::validate_transaction`]
369    pub fn validate_one(
370        &self,
371        origin: TransactionOrigin,
372        transaction: Tx,
373    ) -> TransactionValidationOutcome<Tx> {
374        self.validate_one_with_provider(origin, transaction, &mut None)
375    }
376
377    /// Validates a single transaction with the provided state provider.
378    ///
379    /// This allows reusing the same provider across multiple transaction validations,
380    /// which can improve performance when validating many transactions.
381    ///
382    /// If `state` is `None`, a new state provider will be created.
383    pub fn validate_one_with_state(
384        &self,
385        origin: TransactionOrigin,
386        transaction: Tx,
387        state: &mut Option<Box<dyn AccountInfoReader + Send>>,
388    ) -> TransactionValidationOutcome<Tx> {
389        self.validate_one_with_provider(origin, transaction, state)
390    }
391
392    /// Validates a single transaction using an optional cached state provider.
393    /// If no provider is passed, a new one will be created. This allows reusing
394    /// the same provider across multiple txs.
395    fn validate_one_with_provider(
396        &self,
397        origin: TransactionOrigin,
398        transaction: Tx,
399        maybe_state: &mut Option<Box<dyn AccountInfoReader + Send>>,
400    ) -> TransactionValidationOutcome<Tx> {
401        match self.validate_stateless(origin, &transaction) {
402            Ok(()) => {
403                // stateless checks passed, pass transaction down stateful validation pipeline
404                // If we don't have a state provider yet, fetch the latest state
405                if maybe_state.is_none() {
406                    match self.client.latest() {
407                        Ok(new_state) => {
408                            *maybe_state = Some(Box::new(new_state));
409                        }
410                        Err(err) => {
411                            return TransactionValidationOutcome::Error(
412                                *transaction.hash(),
413                                Box::new(err),
414                            )
415                        }
416                    }
417                }
418
419                let state = maybe_state.as_deref().expect("provider is set");
420
421                self.validate_stateful(origin, transaction, state)
422            }
423            Err(err) => TransactionValidationOutcome::Invalid(transaction, err),
424        }
425    }
426
427    /// Validates a single transaction against the given state provider, performing both
428    /// [stateless](Self::validate_stateless) and [stateful](Self::validate_stateful) checks.
429    pub fn validate_one_with_state_provider(
430        &self,
431        origin: TransactionOrigin,
432        transaction: Tx,
433        state: impl AccountInfoReader,
434    ) -> TransactionValidationOutcome<Tx> {
435        if let Err(err) = self.validate_stateless(origin, &transaction) {
436            return TransactionValidationOutcome::Invalid(transaction, err);
437        }
438        self.validate_stateful(origin, transaction, state)
439    }
440
441    /// Validates a single transaction without requiring any state access (stateless checks only).
442    ///
443    /// Checks tx type support, nonce bounds, size limits, gas limits, fee constraints, chain ID,
444    /// intrinsic gas, and blob tx pre-checks.
445    pub fn validate_stateless(
446        &self,
447        origin: TransactionOrigin,
448        transaction: &Tx,
449    ) -> Result<(), InvalidPoolTransactionError> {
450        // Checks for tx_type
451        match transaction.ty() {
452            // Accept only legacy transactions until EIP-2718/2930 activates
453            EIP2930_TX_TYPE_ID if !self.eip2718 => {
454                return Err(InvalidTransactionError::Eip2930Disabled.into())
455            }
456            // Reject dynamic fee transactions until EIP-1559 activates.
457            EIP1559_TX_TYPE_ID if !self.eip1559 => {
458                return Err(InvalidTransactionError::Eip1559Disabled.into())
459            }
460            // Reject blob transactions.
461            EIP4844_TX_TYPE_ID if !self.eip4844 => {
462                return Err(InvalidTransactionError::Eip4844Disabled.into())
463            }
464            // Reject EIP-7702 transactions.
465            EIP7702_TX_TYPE_ID if !self.eip7702 => {
466                return Err(InvalidTransactionError::Eip7702Disabled.into())
467            }
468            // Accept known transaction types when their respective fork is active
469            LEGACY_TX_TYPE_ID | EIP2930_TX_TYPE_ID | EIP1559_TX_TYPE_ID | EIP4844_TX_TYPE_ID |
470            EIP7702_TX_TYPE_ID => {}
471
472            ty if !self.other_tx_types.bit(ty as usize) => {
473                return Err(InvalidTransactionError::TxTypeNotSupported.into())
474            }
475
476            _ => {}
477        };
478
479        // Reject transactions with a nonce equal to U64::max according to EIP-2681
480        let tx_nonce = transaction.nonce();
481        if tx_nonce == u64::MAX {
482            return Err(InvalidPoolTransactionError::Eip2681)
483        }
484
485        // Reject transactions over defined size to prevent DOS attacks
486        if transaction.is_eip4844() {
487            // Since blob transactions are pulled instead of pushed, and only the consensus data is
488            // kept in memory while the sidecar is cached on disk, there is no critical limit that
489            // should be enforced. Still, enforcing some cap on the input bytes. blob txs also must
490            // be executable right away when they enter the pool.
491            let tx_input_len = transaction.input().len();
492            if tx_input_len > self.max_tx_input_bytes {
493                return Err(InvalidPoolTransactionError::OversizedData {
494                    size: tx_input_len,
495                    limit: self.max_tx_input_bytes,
496                })
497            }
498        } else {
499            // ensure the size of the non-blob transaction
500            let tx_size = transaction.encoded_length();
501            if tx_size > self.max_tx_input_bytes {
502                return Err(InvalidPoolTransactionError::OversizedData {
503                    size: tx_size,
504                    limit: self.max_tx_input_bytes,
505                })
506            }
507        }
508
509        // Check whether the init code size has been exceeded.
510        if self.fork_tracker.is_shanghai_activated() {
511            let max_initcode_size =
512                self.fork_tracker.max_initcode_size.load(std::sync::atomic::Ordering::Relaxed);
513            transaction.ensure_max_init_code_size(max_initcode_size)?;
514        }
515
516        // Checks for gas limit
517        let transaction_gas_limit = transaction.gas_limit();
518        let block_gas_limit = self.max_gas_limit();
519        if transaction_gas_limit > block_gas_limit {
520            return Err(InvalidPoolTransactionError::ExceedsGasLimit(
521                transaction_gas_limit,
522                block_gas_limit,
523            ))
524        }
525
526        // Check individual transaction gas limit if configured
527        if let Some(max_tx_gas_limit) = self.max_tx_gas_limit &&
528            transaction_gas_limit > max_tx_gas_limit
529        {
530            return Err(InvalidPoolTransactionError::MaxTxGasLimitExceeded(
531                transaction_gas_limit,
532                max_tx_gas_limit,
533            ))
534        }
535
536        // Ensure max_priority_fee_per_gas (if EIP1559) is less than max_fee_per_gas if any.
537        if transaction.max_priority_fee_per_gas() > Some(transaction.max_fee_per_gas()) {
538            return Err(InvalidTransactionError::TipAboveFeeCap.into())
539        }
540
541        // determine whether the transaction should be treated as local
542        let is_local = self.local_transactions_config.is_local(origin, transaction.sender_ref());
543
544        // Ensure max possible transaction fee doesn't exceed configured transaction fee cap.
545        // Only for transactions locally submitted for acceptance into the pool.
546        if is_local {
547            match self.tx_fee_cap {
548                Some(0) | None => {} // Skip if cap is 0 or None
549                Some(tx_fee_cap_wei) => {
550                    let max_tx_fee_wei = transaction.cost().saturating_sub(transaction.value());
551                    if max_tx_fee_wei > tx_fee_cap_wei {
552                        return Err(InvalidPoolTransactionError::ExceedsFeeCap {
553                            max_tx_fee_wei: max_tx_fee_wei.saturating_to(),
554                            tx_fee_cap_wei,
555                        })
556                    }
557                }
558            }
559        }
560
561        // Drop non-local transactions with a fee lower than the configured fee for acceptance into
562        // the pool.
563        if !is_local &&
564            transaction.is_dynamic_fee() &&
565            transaction.max_priority_fee_per_gas() < self.minimum_priority_fee
566        {
567            return Err(InvalidPoolTransactionError::PriorityFeeBelowMinimum {
568                minimum_priority_fee: self
569                    .minimum_priority_fee
570                    .expect("minimum priority fee is expected inside if statement"),
571            })
572        }
573
574        // Checks for chainid
575        if let Some(chain_id) = transaction.chain_id() &&
576            chain_id != self.chain_id()
577        {
578            return Err(InvalidTransactionError::ChainIdMismatch.into())
579        }
580
581        if transaction.is_eip7702() {
582            // Prague fork is required for 7702 txs
583            if !self.fork_tracker.is_prague_activated() {
584                return Err(InvalidTransactionError::TxTypeNotSupported.into())
585            }
586
587            if transaction.authorization_list().is_none_or(|l| l.is_empty()) {
588                return Err(Eip7702PoolTransactionError::MissingEip7702AuthorizationList.into())
589            }
590        }
591
592        ensure_intrinsic_gas(transaction, &self.fork_tracker)?;
593
594        // light blob tx pre-checks
595        if transaction.is_eip4844() {
596            // Cancun fork is required for blob txs
597            if !self.fork_tracker.is_cancun_activated() {
598                return Err(InvalidTransactionError::TxTypeNotSupported.into())
599            }
600
601            let blob_count = transaction.blob_count().unwrap_or(0);
602            if blob_count == 0 {
603                // no blobs
604                return Err(InvalidPoolTransactionError::Eip4844(
605                    Eip4844PoolTransactionError::NoEip4844Blobs,
606                ))
607            }
608
609            let max_blob_count = self.fork_tracker.max_blob_count();
610            if blob_count > max_blob_count {
611                return Err(InvalidPoolTransactionError::Eip4844(
612                    Eip4844PoolTransactionError::TooManyEip4844Blobs {
613                        have: blob_count,
614                        permitted: max_blob_count,
615                    },
616                ))
617            }
618        }
619
620        // Transaction gas limit validation (EIP-7825 for Osaka+)
621        let tx_gas_limit_cap =
622            self.fork_tracker.tx_gas_limit_cap.load(std::sync::atomic::Ordering::Relaxed);
623        if tx_gas_limit_cap > 0 && transaction.gas_limit() > tx_gas_limit_cap {
624            return Err(InvalidTransactionError::GasLimitTooHigh.into())
625        }
626
627        // Run additional stateless validation if configured
628        if let Some(check) = &self.additional_stateless_validation {
629            check(origin, transaction)?;
630        }
631
632        Ok(())
633    }
634
635    /// Validates a single transaction against the given state (stateful checks only).
636    ///
637    /// Checks sender account balance, nonce, bytecode, and validates blob sidecars. The
638    /// transaction must have already passed [`validate_stateless`](Self::validate_stateless).
639    pub fn validate_stateful<P>(
640        &self,
641        origin: TransactionOrigin,
642        mut transaction: Tx,
643        state: P,
644    ) -> TransactionValidationOutcome<Tx>
645    where
646        P: AccountInfoReader,
647    {
648        // Use provider to get account info
649        let account = match state.basic_account(transaction.sender_ref()) {
650            Ok(account) => account.unwrap_or_default(),
651            Err(err) => {
652                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err))
653            }
654        };
655
656        // check for bytecode
657        match self.validate_sender_bytecode(&transaction, &account, &state) {
658            Err(outcome) => return outcome,
659            Ok(Err(err)) => return TransactionValidationOutcome::Invalid(transaction, err),
660            _ => {}
661        };
662
663        // Checks for nonce
664        if transaction.requires_nonce_check() &&
665            let Err(err) = self.validate_sender_nonce(&transaction, &account)
666        {
667            return TransactionValidationOutcome::Invalid(transaction, err)
668        }
669
670        // checks for max cost not exceedng account_balance
671        if let Err(err) = self.validate_sender_balance(&transaction, &account) {
672            return TransactionValidationOutcome::Invalid(transaction, err)
673        }
674
675        // heavy blob tx validation
676        let maybe_blob_sidecar = match self.validate_eip4844(&mut transaction) {
677            Err(err) => return TransactionValidationOutcome::Invalid(transaction, err),
678            Ok(sidecar) => sidecar,
679        };
680
681        // Run additional stateful validation if configured
682        if let Some(check) = &self.additional_stateful_validation &&
683            let Err(err) = check(origin, &transaction, &state)
684        {
685            return TransactionValidationOutcome::Invalid(transaction, err)
686        }
687
688        let authorities = self.recover_authorities(&transaction);
689        // Return the valid transaction
690        TransactionValidationOutcome::Valid {
691            balance: account.balance,
692            state_nonce: account.nonce,
693            bytecode_hash: account.bytecode_hash,
694            transaction: ValidTransaction::new(transaction, maybe_blob_sidecar),
695            // by this point assume all external transactions should be propagated
696            propagate: match origin {
697                TransactionOrigin::External => true,
698                TransactionOrigin::Local => {
699                    self.local_transactions_config.propagate_local_transactions
700                }
701                TransactionOrigin::Private => false,
702            },
703            authorities,
704        }
705    }
706
707    /// Validates that the sender’s account has valid or no bytecode.
708    pub fn validate_sender_bytecode(
709        &self,
710        transaction: &Tx,
711        sender: &Account,
712        state: impl BytecodeReader,
713    ) -> Result<Result<(), InvalidPoolTransactionError>, TransactionValidationOutcome<Tx>> {
714        // Unless Prague is active, the signer account shouldn't have bytecode.
715        //
716        // If Prague is active, only EIP-7702 bytecode is allowed for the sender.
717        //
718        // Any other case means that the account is not an EOA, and should not be able to send
719        // transactions.
720        if let Some(code_hash) = &sender.bytecode_hash {
721            let is_eip7702 = if self.fork_tracker.is_prague_activated() {
722                match state.bytecode_by_hash(code_hash) {
723                    Ok(bytecode) => bytecode.unwrap_or_default().is_eip7702(),
724                    Err(err) => {
725                        return Err(TransactionValidationOutcome::Error(
726                            *transaction.hash(),
727                            Box::new(err),
728                        ))
729                    }
730                }
731            } else {
732                false
733            };
734
735            if !is_eip7702 {
736                return Ok(Err(InvalidTransactionError::SignerAccountHasBytecode.into()))
737            }
738        }
739        Ok(Ok(()))
740    }
741
742    /// Checks if the transaction nonce is valid.
743    pub fn validate_sender_nonce(
744        &self,
745        transaction: &Tx,
746        sender: &Account,
747    ) -> Result<(), InvalidPoolTransactionError> {
748        let tx_nonce = transaction.nonce();
749
750        if tx_nonce < sender.nonce {
751            return Err(InvalidTransactionError::NonceNotConsistent {
752                tx: tx_nonce,
753                state: sender.nonce,
754            }
755            .into())
756        }
757        Ok(())
758    }
759
760    /// Ensures the sender has sufficient account balance.
761    pub fn validate_sender_balance(
762        &self,
763        transaction: &Tx,
764        sender: &Account,
765    ) -> Result<(), InvalidPoolTransactionError> {
766        let cost = transaction.cost();
767
768        if !self.disable_balance_check && cost > &sender.balance {
769            let expected = *cost;
770            return Err(InvalidTransactionError::InsufficientFunds(
771                GotExpected { got: sender.balance, expected }.into(),
772            )
773            .into())
774        }
775        Ok(())
776    }
777
778    /// Validates EIP-4844 blob sidecar data and returns the extracted sidecar, if any.
779    pub fn validate_eip4844(
780        &self,
781        transaction: &mut Tx,
782    ) -> Result<Option<BlobTransactionSidecarVariant>, InvalidPoolTransactionError> {
783        let mut maybe_blob_sidecar = None;
784
785        // heavy blob tx validation
786        if transaction.is_eip4844() {
787            // extract the blob from the transaction
788            match transaction.take_blob() {
789                EthBlobTransactionSidecar::None => {
790                    // this should not happen
791                    return Err(InvalidTransactionError::TxTypeNotSupported.into())
792                }
793                EthBlobTransactionSidecar::Missing => {
794                    // This can happen for re-injected blob transactions (on re-org), since the blob
795                    // is stripped from the transaction and not included in a block.
796                    // check if the blob is in the store, if it's included we previously validated
797                    // it and inserted it
798                    if self.blob_store.contains(*transaction.hash()).is_ok_and(|c| c) {
799                        // validated transaction is already in the store
800                    } else {
801                        return Err(InvalidPoolTransactionError::Eip4844(
802                            Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
803                        ))
804                    }
805                }
806                EthBlobTransactionSidecar::Present(sidecar) => {
807                    let now = Instant::now();
808
809                    // EIP-7594 sidecar version handling
810                    if self.eip7594 {
811                        // Standard Ethereum behavior
812                        if self.fork_tracker.is_osaka_activated() {
813                            if sidecar.is_eip4844() {
814                                return Err(InvalidPoolTransactionError::Eip4844(
815                                    Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka,
816                                ))
817                            }
818                        } else if sidecar.is_eip7594() && !self.allow_7594_sidecars() {
819                            return Err(InvalidPoolTransactionError::Eip4844(
820                                Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka,
821                            ))
822                        }
823                    } else {
824                        // EIP-7594 disabled: always reject v1 sidecars, accept v0
825                        if sidecar.is_eip7594() {
826                            return Err(InvalidPoolTransactionError::Eip4844(
827                                Eip4844PoolTransactionError::Eip7594SidecarDisallowed,
828                            ))
829                        }
830                    }
831
832                    // validate the blob
833                    if let Err(err) = transaction.validate_blob(&sidecar, self.kzg_settings.get()) {
834                        return Err(InvalidPoolTransactionError::Eip4844(
835                            Eip4844PoolTransactionError::InvalidEip4844Blob(err),
836                        ))
837                    }
838                    // Record the duration of successful blob validation as histogram
839                    self.validation_metrics.blob_validation_duration.record(now.elapsed());
840                    // store the extracted blob
841                    maybe_blob_sidecar = Some(sidecar);
842                }
843            }
844        }
845        Ok(maybe_blob_sidecar)
846    }
847
848    /// Returns the recovered authorities for the given transaction
849    fn recover_authorities(&self, transaction: &Tx) -> std::option::Option<Vec<Address>> {
850        transaction
851            .authorization_list()
852            .map(|auths| auths.iter().flat_map(|auth| auth.recover_authority()).collect::<Vec<_>>())
853    }
854
855    /// Validates all given transactions.
856    fn validate_batch(
857        &self,
858        transactions: impl IntoIterator<Item = (TransactionOrigin, Tx)>,
859    ) -> Vec<TransactionValidationOutcome<Tx>> {
860        let mut provider = None;
861        transactions
862            .into_iter()
863            .map(|(origin, tx)| self.validate_one_with_provider(origin, tx, &mut provider))
864            .collect()
865    }
866
867    /// Validates all given transactions with origin.
868    fn validate_batch_with_origin(
869        &self,
870        origin: TransactionOrigin,
871        transactions: impl IntoIterator<Item = Tx> + Send,
872    ) -> Vec<TransactionValidationOutcome<Tx>> {
873        let mut provider = None;
874        transactions
875            .into_iter()
876            .map(|tx| self.validate_one_with_provider(origin, tx, &mut provider))
877            .collect()
878    }
879
880    fn on_new_head_block(&self, new_tip_block: &HeaderTy<Evm::Primitives>) {
881        // update all forks
882        if self.chain_spec().is_shanghai_active_at_timestamp(new_tip_block.timestamp()) {
883            self.fork_tracker.shanghai.store(true, std::sync::atomic::Ordering::Relaxed);
884        }
885
886        if self.chain_spec().is_cancun_active_at_timestamp(new_tip_block.timestamp()) {
887            self.fork_tracker.cancun.store(true, std::sync::atomic::Ordering::Relaxed);
888        }
889
890        if self.chain_spec().is_prague_active_at_timestamp(new_tip_block.timestamp()) {
891            self.fork_tracker.prague.store(true, std::sync::atomic::Ordering::Relaxed);
892        }
893
894        if self.chain_spec().is_osaka_active_at_timestamp(new_tip_block.timestamp()) {
895            self.fork_tracker.osaka.store(true, std::sync::atomic::Ordering::Relaxed);
896        }
897
898        self.fork_tracker
899            .tip_timestamp
900            .store(new_tip_block.timestamp(), std::sync::atomic::Ordering::Relaxed);
901
902        if let Some(blob_params) =
903            self.chain_spec().blob_params_at_timestamp(new_tip_block.timestamp())
904        {
905            self.fork_tracker
906                .max_blob_count
907                .store(blob_params.max_blobs_per_tx, std::sync::atomic::Ordering::Relaxed);
908        }
909
910        self.block_gas_limit.store(new_tip_block.gas_limit(), std::sync::atomic::Ordering::Relaxed);
911
912        // Get EVM limits from evm_config.evm_env()
913        let evm_env = self
914            .evm_config
915            .evm_env(new_tip_block)
916            .expect("evm_env should not fail for executed block");
917
918        self.fork_tracker
919            .max_initcode_size
920            .store(evm_env.cfg_env.max_initcode_size(), std::sync::atomic::Ordering::Relaxed);
921        // EIP-8037: When state gas is enabled, `tx.gas` can exceed the per-tx gas limit cap
922        // because the cap only applies to regular gas (state gas uses a reservoir).
923        // Store 0 to disable the txpool-level check.
924        let tx_gas_limit_cap = if evm_env.cfg_env.is_amsterdam_eip8037_enabled() {
925            0
926        } else {
927            evm_env.cfg_env.tx_gas_limit_cap()
928        };
929        self.fork_tracker
930            .tx_gas_limit_cap
931            .store(tx_gas_limit_cap, std::sync::atomic::Ordering::Relaxed);
932    }
933
934    fn max_gas_limit(&self) -> u64 {
935        self.block_gas_limit.load(std::sync::atomic::Ordering::Relaxed)
936    }
937
938    /// Returns whether EIP-7594 sidecars are allowed
939    fn allow_7594_sidecars(&self) -> bool {
940        let tip_timestamp = self.fork_tracker.tip_timestamp();
941
942        // If next block is Osaka, allow 7594 sidecars
943        if self.chain_spec().is_osaka_active_at_timestamp(tip_timestamp.saturating_add(12)) {
944            true
945        } else if self.chain_spec().is_osaka_active_at_timestamp(tip_timestamp.saturating_add(24)) {
946            let current_timestamp =
947                SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
948
949            // Allow after 4 seconds into last non-Osaka slot
950            current_timestamp >= tip_timestamp.saturating_add(4)
951        } else {
952            false
953        }
954    }
955}
956
957impl<Client, Tx, Evm> TransactionValidator for EthTransactionValidator<Client, Tx, Evm>
958where
959    Client: ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks> + StateProviderFactory,
960    Tx: EthPoolTransaction,
961    Evm: ConfigureEvm,
962{
963    type Transaction = Tx;
964    type Block = BlockTy<Evm::Primitives>;
965
966    async fn validate_transaction(
967        &self,
968        origin: TransactionOrigin,
969        transaction: Self::Transaction,
970    ) -> TransactionValidationOutcome<Self::Transaction> {
971        self.validate_one(origin, transaction)
972    }
973
974    async fn validate_transactions(
975        &self,
976        transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction), IntoIter: Send>
977            + Send,
978    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
979        self.validate_batch(transactions)
980    }
981
982    async fn validate_transactions_with_origin(
983        &self,
984        origin: TransactionOrigin,
985        transactions: impl IntoIterator<Item = Self::Transaction, IntoIter: Send> + Send,
986    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
987        self.validate_batch_with_origin(origin, transactions)
988    }
989
990    fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
991        Self::on_new_head_block(self, new_tip_block.header())
992    }
993}
994
995/// A builder for [`EthTransactionValidator`] and [`TransactionValidationTaskExecutor`]
996#[derive(Debug)]
997pub struct EthTransactionValidatorBuilder<Client, Evm> {
998    client: Client,
999    /// The EVM configuration to use for validation.
1000    evm_config: Evm,
1001    /// Fork indicator whether we are in the Shanghai stage.
1002    shanghai: bool,
1003    /// Fork indicator whether we are in the Cancun hardfork.
1004    cancun: bool,
1005    /// Fork indicator whether we are in the Prague hardfork.
1006    prague: bool,
1007    /// Fork indicator whether we are in the Osaka hardfork.
1008    osaka: bool,
1009    /// Timestamp of the tip block.
1010    tip_timestamp: u64,
1011    /// Max blob count at the block's timestamp.
1012    max_blob_count: u64,
1013    /// Whether using EIP-2718 type transactions is allowed
1014    eip2718: bool,
1015    /// Whether using EIP-1559 type transactions is allowed
1016    eip1559: bool,
1017    /// Whether using EIP-4844 type transactions is allowed
1018    eip4844: bool,
1019    /// Whether using EIP-7702 type transactions is allowed
1020    eip7702: bool,
1021    /// The current max gas limit
1022    block_gas_limit: AtomicU64,
1023    /// The current tx fee cap limit in wei locally submitted into the pool.
1024    tx_fee_cap: Option<u128>,
1025    /// Minimum priority fee to enforce for acceptance into the pool.
1026    minimum_priority_fee: Option<u128>,
1027    /// Determines how many additional tasks to spawn
1028    ///
1029    /// Default is 1
1030    additional_tasks: usize,
1031
1032    /// Stores the setup and parameters needed for validating KZG proofs.
1033    kzg_settings: EnvKzgSettings,
1034    /// How to handle [`TransactionOrigin::Local`](TransactionOrigin) transactions.
1035    local_transactions_config: LocalTransactionConfig,
1036    /// Max size in bytes of a single transaction allowed
1037    max_tx_input_bytes: usize,
1038    /// Maximum gas limit for individual transactions
1039    max_tx_gas_limit: Option<u64>,
1040    /// Disable balance checks during transaction validation
1041    disable_balance_check: bool,
1042    /// Bitmap of custom transaction types that are allowed.
1043    other_tx_types: U256,
1044    /// Cached max initcode size from EVM config
1045    max_initcode_size: usize,
1046    /// Cached transaction gas limit cap from EVM config (0 = no cap)
1047    tx_gas_limit_cap: u64,
1048    /// Whether EIP-7594 blob sidecars are accepted.
1049    /// When false, EIP-7594 (v1) sidecars are always rejected and EIP-4844 (v0) sidecars
1050    /// are always accepted, regardless of Osaka fork activation.
1051    eip7594: bool,
1052}
1053
1054impl<Client, Evm> EthTransactionValidatorBuilder<Client, Evm> {
1055    /// Creates a new builder for the given client and EVM config
1056    ///
1057    /// By default this assumes the network is on the `Prague` hardfork and the following
1058    /// transactions are allowed:
1059    ///  - Legacy
1060    ///  - EIP-2718
1061    ///  - EIP-1559
1062    ///  - EIP-4844
1063    ///  - EIP-7702
1064    pub fn new(client: Client, evm_config: Evm) -> Self
1065    where
1066        Client: ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks>
1067            + BlockReaderIdExt<Header = HeaderTy<Evm::Primitives>>,
1068        Evm: ConfigureEvm,
1069    {
1070        let chain_spec = client.chain_spec();
1071        let tip = client
1072            .header_by_id(BlockId::latest())
1073            .expect("failed to fetch latest header")
1074            .expect("latest header is not found");
1075        let evm_env =
1076            evm_config.evm_env(&tip).expect("evm_env should not fail for existing blocks");
1077
1078        Self {
1079            block_gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M.into(),
1080            client,
1081            evm_config,
1082            minimum_priority_fee: None,
1083            additional_tasks: 1,
1084            kzg_settings: EnvKzgSettings::Default,
1085            local_transactions_config: Default::default(),
1086            max_tx_input_bytes: DEFAULT_MAX_TX_INPUT_BYTES,
1087            tx_fee_cap: Some(1e18 as u128),
1088            max_tx_gas_limit: None,
1089            // by default all transaction types are allowed
1090            eip2718: true,
1091            eip1559: true,
1092            eip4844: true,
1093            eip7702: true,
1094
1095            shanghai: chain_spec.is_shanghai_active_at_timestamp(tip.timestamp()),
1096            cancun: chain_spec.is_cancun_active_at_timestamp(tip.timestamp()),
1097            prague: chain_spec.is_prague_active_at_timestamp(tip.timestamp()),
1098            osaka: chain_spec.is_osaka_active_at_timestamp(tip.timestamp()),
1099
1100            tip_timestamp: tip.timestamp(),
1101
1102            max_blob_count: chain_spec
1103                .blob_params_at_timestamp(tip.timestamp())
1104                .unwrap_or_else(BlobParams::prague)
1105                .max_blobs_per_tx,
1106
1107            // balance checks are enabled by default
1108            disable_balance_check: false,
1109
1110            // no custom transaction types by default
1111            other_tx_types: U256::ZERO,
1112
1113            // EIP-8037: When state gas is enabled, tx.gas can exceed the per-tx cap
1114            tx_gas_limit_cap: if evm_env.cfg_env.is_amsterdam_eip8037_enabled() {
1115                0
1116            } else {
1117                evm_env.cfg_env.tx_gas_limit_cap()
1118            },
1119            max_initcode_size: evm_env.cfg_env.max_initcode_size(),
1120
1121            // EIP-7594 sidecars are accepted by default (standard Ethereum behavior)
1122            eip7594: true,
1123        }
1124    }
1125
1126    /// Disables the Cancun fork.
1127    pub const fn no_cancun(self) -> Self {
1128        self.set_cancun(false)
1129    }
1130
1131    /// Whether to allow exemptions for local transaction exemptions.
1132    pub fn with_local_transactions_config(
1133        mut self,
1134        local_transactions_config: LocalTransactionConfig,
1135    ) -> Self {
1136        self.local_transactions_config = local_transactions_config;
1137        self
1138    }
1139
1140    /// Set the Cancun fork.
1141    pub const fn set_cancun(mut self, cancun: bool) -> Self {
1142        self.cancun = cancun;
1143        self
1144    }
1145
1146    /// Disables the Shanghai fork.
1147    pub const fn no_shanghai(self) -> Self {
1148        self.set_shanghai(false)
1149    }
1150
1151    /// Set the Shanghai fork.
1152    pub const fn set_shanghai(mut self, shanghai: bool) -> Self {
1153        self.shanghai = shanghai;
1154        self
1155    }
1156
1157    /// Disables the Prague fork.
1158    pub const fn no_prague(self) -> Self {
1159        self.set_prague(false)
1160    }
1161
1162    /// Set the Prague fork.
1163    pub const fn set_prague(mut self, prague: bool) -> Self {
1164        self.prague = prague;
1165        self
1166    }
1167
1168    /// Disables the Osaka fork.
1169    pub const fn no_osaka(self) -> Self {
1170        self.set_osaka(false)
1171    }
1172
1173    /// Set the Osaka fork.
1174    pub const fn set_osaka(mut self, osaka: bool) -> Self {
1175        self.osaka = osaka;
1176        self
1177    }
1178
1179    /// Disables the support for EIP-2718 transactions.
1180    pub const fn no_eip2718(self) -> Self {
1181        self.set_eip2718(false)
1182    }
1183
1184    /// Set the support for EIP-2718 transactions.
1185    pub const fn set_eip2718(mut self, eip2718: bool) -> Self {
1186        self.eip2718 = eip2718;
1187        self
1188    }
1189
1190    /// Disables the support for EIP-1559 transactions.
1191    pub const fn no_eip1559(self) -> Self {
1192        self.set_eip1559(false)
1193    }
1194
1195    /// Set the support for EIP-1559 transactions.
1196    pub const fn set_eip1559(mut self, eip1559: bool) -> Self {
1197        self.eip1559 = eip1559;
1198        self
1199    }
1200
1201    /// Disables the support for EIP-4844 transactions.
1202    pub const fn no_eip4844(self) -> Self {
1203        self.set_eip4844(false)
1204    }
1205
1206    /// Set the support for EIP-4844 transactions.
1207    pub const fn set_eip4844(mut self, eip4844: bool) -> Self {
1208        self.eip4844 = eip4844;
1209        self
1210    }
1211
1212    /// Disables the support for EIP-7702 transactions.
1213    pub const fn no_eip7702(self) -> Self {
1214        self.set_eip7702(false)
1215    }
1216
1217    /// Set the support for EIP-7702 transactions.
1218    pub const fn set_eip7702(mut self, eip7702: bool) -> Self {
1219        self.eip7702 = eip7702;
1220        self
1221    }
1222
1223    /// Disables EIP-7594 blob sidecar support.
1224    ///
1225    /// When disabled, EIP-7594 (v1) blob sidecars are always rejected and EIP-4844 (v0)
1226    /// sidecars are always accepted, regardless of Osaka fork activation.
1227    ///
1228    /// Use this for chains that do not adopt EIP-7594 (`PeerDAS`).
1229    pub const fn no_eip7594(self) -> Self {
1230        self.set_eip7594(false)
1231    }
1232
1233    /// Set EIP-7594 blob sidecar support.
1234    ///
1235    /// When true (default), standard Ethereum behavior applies: v0 sidecars before Osaka,
1236    /// v1 sidecars after Osaka. When false, v1 sidecars are always rejected.
1237    pub const fn set_eip7594(mut self, eip7594: bool) -> Self {
1238        self.eip7594 = eip7594;
1239        self
1240    }
1241
1242    /// Sets the [`EnvKzgSettings`] to use for validating KZG proofs.
1243    pub fn kzg_settings(mut self, kzg_settings: EnvKzgSettings) -> Self {
1244        self.kzg_settings = kzg_settings;
1245        self
1246    }
1247
1248    /// Sets a minimum priority fee that's enforced for acceptance into the pool.
1249    pub const fn with_minimum_priority_fee(mut self, minimum_priority_fee: Option<u128>) -> Self {
1250        self.minimum_priority_fee = minimum_priority_fee;
1251        self
1252    }
1253
1254    /// Sets the number of additional tasks to spawn.
1255    pub const fn with_additional_tasks(mut self, additional_tasks: usize) -> Self {
1256        self.additional_tasks = additional_tasks;
1257        self
1258    }
1259
1260    /// Sets a max size in bytes of a single transaction allowed into the pool
1261    pub const fn with_max_tx_input_bytes(mut self, max_tx_input_bytes: usize) -> Self {
1262        self.max_tx_input_bytes = max_tx_input_bytes;
1263        self
1264    }
1265
1266    /// Sets the block gas limit
1267    ///
1268    /// Transactions with a gas limit greater than this will be rejected.
1269    pub fn set_block_gas_limit(self, block_gas_limit: u64) -> Self {
1270        self.block_gas_limit.store(block_gas_limit, std::sync::atomic::Ordering::Relaxed);
1271        self
1272    }
1273
1274    /// Sets the block gas limit
1275    ///
1276    /// Transactions with a gas limit greater than this will be rejected.
1277    pub const fn set_tx_fee_cap(mut self, tx_fee_cap: u128) -> Self {
1278        self.tx_fee_cap = Some(tx_fee_cap);
1279        self
1280    }
1281
1282    /// Sets the maximum gas limit for individual transactions
1283    pub const fn with_max_tx_gas_limit(mut self, max_tx_gas_limit: Option<u64>) -> Self {
1284        self.max_tx_gas_limit = max_tx_gas_limit;
1285        self
1286    }
1287
1288    /// Disables balance checks during transaction validation
1289    pub const fn disable_balance_check(mut self) -> Self {
1290        self.disable_balance_check = true;
1291        self
1292    }
1293
1294    /// Adds a custom transaction type to the validator.
1295    pub const fn with_custom_tx_type(mut self, tx_type: u8) -> Self {
1296        self.other_tx_types.set_bit(tx_type as usize, true);
1297        self
1298    }
1299
1300    /// Builds a the [`EthTransactionValidator`] without spawning validator tasks.
1301    pub fn build<Tx, S>(self, blob_store: S) -> EthTransactionValidator<Client, Tx, Evm>
1302    where
1303        S: BlobStore,
1304    {
1305        let Self {
1306            client,
1307            evm_config,
1308            shanghai,
1309            cancun,
1310            prague,
1311            osaka,
1312            tip_timestamp,
1313            eip2718,
1314            eip1559,
1315            eip4844,
1316            eip7702,
1317            block_gas_limit,
1318            tx_fee_cap,
1319            minimum_priority_fee,
1320            kzg_settings,
1321            local_transactions_config,
1322            max_tx_input_bytes,
1323            max_tx_gas_limit,
1324            disable_balance_check,
1325            max_blob_count,
1326            additional_tasks: _,
1327            other_tx_types,
1328            max_initcode_size,
1329            tx_gas_limit_cap,
1330            eip7594,
1331        } = self;
1332
1333        let fork_tracker = ForkTracker {
1334            shanghai: AtomicBool::new(shanghai),
1335            cancun: AtomicBool::new(cancun),
1336            prague: AtomicBool::new(prague),
1337            osaka: AtomicBool::new(osaka),
1338            tip_timestamp: AtomicU64::new(tip_timestamp),
1339            max_blob_count: AtomicU64::new(max_blob_count),
1340            max_initcode_size: AtomicUsize::new(max_initcode_size),
1341            tx_gas_limit_cap: AtomicU64::new(tx_gas_limit_cap),
1342        };
1343
1344        EthTransactionValidator {
1345            client,
1346            eip2718,
1347            eip1559,
1348            fork_tracker,
1349            eip4844,
1350            eip7702,
1351            block_gas_limit,
1352            tx_fee_cap,
1353            minimum_priority_fee,
1354            blob_store: Box::new(blob_store),
1355            kzg_settings,
1356            local_transactions_config,
1357            max_tx_input_bytes,
1358            max_tx_gas_limit,
1359            disable_balance_check,
1360            evm_config,
1361            _marker: Default::default(),
1362            validation_metrics: TxPoolValidationMetrics::default(),
1363            other_tx_types,
1364            eip7594,
1365            additional_stateless_validation: None,
1366            additional_stateful_validation: None,
1367        }
1368    }
1369
1370    /// Builds a [`EthTransactionValidator`] and spawns validation tasks via the
1371    /// [`TransactionValidationTaskExecutor`]
1372    ///
1373    /// The validator will spawn `additional_tasks` additional tasks for validation.
1374    ///
1375    /// By default this will spawn 1 additional task.
1376    pub fn build_with_tasks<Tx, S>(
1377        self,
1378        tasks: Runtime,
1379        blob_store: S,
1380    ) -> TransactionValidationTaskExecutor<EthTransactionValidator<Client, Tx, Evm>>
1381    where
1382        S: BlobStore,
1383    {
1384        let additional_tasks = self.additional_tasks;
1385        let validator = self.build::<Tx, S>(blob_store);
1386        TransactionValidationTaskExecutor::spawn(validator, &tasks, additional_tasks)
1387    }
1388}
1389
1390/// Keeps track of whether certain forks are activated
1391#[derive(Debug)]
1392pub struct ForkTracker {
1393    /// Tracks if shanghai is activated at the block's timestamp.
1394    pub shanghai: AtomicBool,
1395    /// Tracks if cancun is activated at the block's timestamp.
1396    pub cancun: AtomicBool,
1397    /// Tracks if prague is activated at the block's timestamp.
1398    pub prague: AtomicBool,
1399    /// Tracks if osaka is activated at the block's timestamp.
1400    pub osaka: AtomicBool,
1401    /// Tracks max blob count per transaction at the block's timestamp.
1402    pub max_blob_count: AtomicU64,
1403    /// Tracks the timestamp of the tip block.
1404    pub tip_timestamp: AtomicU64,
1405    /// Cached max initcode size from EVM config
1406    pub max_initcode_size: AtomicUsize,
1407    /// Cached transaction gas limit cap from EVM config (0 = no cap)
1408    pub tx_gas_limit_cap: AtomicU64,
1409}
1410
1411impl ForkTracker {
1412    /// Returns `true` if Shanghai fork is activated.
1413    pub fn is_shanghai_activated(&self) -> bool {
1414        self.shanghai.load(std::sync::atomic::Ordering::Relaxed)
1415    }
1416
1417    /// Returns `true` if Cancun fork is activated.
1418    pub fn is_cancun_activated(&self) -> bool {
1419        self.cancun.load(std::sync::atomic::Ordering::Relaxed)
1420    }
1421
1422    /// Returns `true` if Prague fork is activated.
1423    pub fn is_prague_activated(&self) -> bool {
1424        self.prague.load(std::sync::atomic::Ordering::Relaxed)
1425    }
1426
1427    /// Returns `true` if Osaka fork is activated.
1428    pub fn is_osaka_activated(&self) -> bool {
1429        self.osaka.load(std::sync::atomic::Ordering::Relaxed)
1430    }
1431
1432    /// Returns the timestamp of the tip block.
1433    pub fn tip_timestamp(&self) -> u64 {
1434        self.tip_timestamp.load(std::sync::atomic::Ordering::Relaxed)
1435    }
1436
1437    /// Returns the max allowed blob count per transaction.
1438    pub fn max_blob_count(&self) -> u64 {
1439        self.max_blob_count.load(std::sync::atomic::Ordering::Relaxed)
1440    }
1441}
1442
1443/// Ensures that gas limit of the transaction exceeds the intrinsic gas of the transaction.
1444///
1445/// Caution: This only checks past the Merge hardfork.
1446pub fn ensure_intrinsic_gas<T: EthPoolTransaction>(
1447    transaction: &T,
1448    fork_tracker: &ForkTracker,
1449) -> Result<(), InvalidPoolTransactionError> {
1450    use revm_primitives::hardfork::SpecId;
1451    let spec_id = if fork_tracker.is_prague_activated() {
1452        SpecId::PRAGUE
1453    } else if fork_tracker.is_shanghai_activated() {
1454        SpecId::SHANGHAI
1455    } else {
1456        SpecId::MERGE
1457    };
1458
1459    let gas = revm_interpreter::gas::calculate_initial_tx_gas(
1460        spec_id,
1461        transaction.input(),
1462        transaction.is_create(),
1463        transaction.access_list().map(|l| l.len()).unwrap_or_default() as u64,
1464        transaction
1465            .access_list()
1466            .map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
1467            .unwrap_or_default() as u64,
1468        transaction.authorization_list().map(|l| l.len()).unwrap_or_default() as u64,
1469    );
1470
1471    let gas_limit = transaction.gas_limit();
1472    if gas_limit < gas.initial_total_gas() || gas_limit < gas.floor_gas {
1473        Err(InvalidPoolTransactionError::IntrinsicGasTooLow)
1474    } else {
1475        Ok(())
1476    }
1477}
1478
1479#[cfg(test)]
1480mod tests {
1481    use super::*;
1482    use crate::{
1483        blobstore::InMemoryBlobStore, error::PoolErrorKind, traits::PoolTransaction,
1484        CoinbaseTipOrdering, EthPooledTransaction, Pool, TransactionPool,
1485    };
1486    use alloy_consensus::Transaction;
1487    use alloy_eips::eip2718::Decodable2718;
1488    use alloy_primitives::{hex, U256};
1489    use reth_ethereum_primitives::PooledTransactionVariant;
1490    use reth_evm_ethereum::EthEvmConfig;
1491    use reth_primitives_traits::SignedTransaction;
1492    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
1493    use revm_primitives::eip3860::MAX_INITCODE_SIZE;
1494
1495    fn test_evm_config() -> EthEvmConfig {
1496        EthEvmConfig::mainnet()
1497    }
1498
1499    fn get_transaction() -> EthPooledTransaction {
1500        let raw = "0x02f914950181ad84b2d05e0085117553845b830f7df88080b9143a6040608081523462000414576200133a803803806200001e8162000419565b9283398101608082820312620004145781516001600160401b03908181116200041457826200004f9185016200043f565b92602092838201519083821162000414576200006d9183016200043f565b8186015190946001600160a01b03821692909183900362000414576060015190805193808511620003145760038054956001938488811c9816801562000409575b89891014620003f3578190601f988981116200039d575b50899089831160011462000336576000926200032a575b505060001982841b1c191690841b1781555b8751918211620003145760049788548481811c9116801562000309575b89821014620002f457878111620002a9575b5087908784116001146200023e5793839491849260009562000232575b50501b92600019911b1c19161785555b6005556007805460ff60a01b19169055600880546001600160a01b0319169190911790553015620001f3575060025469d3c21bcecceda100000092838201809211620001de57506000917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9160025530835282815284832084815401905584519384523093a351610e889081620004b28239f35b601190634e487b7160e01b6000525260246000fd5b90606493519262461bcd60e51b845283015260248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152fd5b0151935038806200013a565b9190601f198416928a600052848a6000209460005b8c8983831062000291575050501062000276575b50505050811b0185556200014a565b01519060f884600019921b161c191690553880808062000267565b86860151895590970196948501948893500162000253565b89600052886000208880860160051c8201928b8710620002ea575b0160051c019085905b828110620002dd5750506200011d565b60008155018590620002cd565b92508192620002c4565b60228a634e487b7160e01b6000525260246000fd5b90607f16906200010b565b634e487b7160e01b600052604160045260246000fd5b015190503880620000dc565b90869350601f19831691856000528b6000209260005b8d8282106200038657505084116200036d575b505050811b018155620000ee565b015160001983861b60f8161c191690553880806200035f565b8385015186558a979095019493840193016200034c565b90915083600052896000208980850160051c8201928c8610620003e9575b918891869594930160051c01915b828110620003d9575050620000c5565b60008155859450889101620003c9565b92508192620003bb565b634e487b7160e01b600052602260045260246000fd5b97607f1697620000ae565b600080fd5b6040519190601f01601f191682016001600160401b038111838210176200031457604052565b919080601f84011215620004145782516001600160401b038111620003145760209062000475601f8201601f1916830162000419565b92818452828287010111620004145760005b8181106200049d57508260009394955001015290565b85810183015184820184015282016200048756fe608060408181526004918236101561001657600080fd5b600092833560e01c91826306fdde0314610a1c57508163095ea7b3146109f257816318160ddd146109d35781631b4c84d2146109ac57816323b872dd14610833578163313ce5671461081757816339509351146107c357816370a082311461078c578163715018a6146107685781638124f7ac146107495781638da5cb5b1461072057816395d89b411461061d578163a457c2d714610575578163a9059cbb146104e4578163c9567bf914610120575063dd62ed3e146100d557600080fd5b3461011c578060031936011261011c57806020926100f1610b5a565b6100f9610b75565b6001600160a01b0391821683526001865283832091168252845220549051908152f35b5080fd5b905082600319360112610338576008546001600160a01b039190821633036104975760079283549160ff8360a01c1661045557737a250d5630b4cf539739df2c5dacb4c659f2488d92836bffffffffffffffffffffffff60a01b8092161786553087526020938785528388205430156104065730895260018652848920828a52865280858a205584519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925863092a38554835163c45a015560e01b815290861685828581845afa9182156103dd57849187918b946103e7575b5086516315ab88c960e31b815292839182905afa9081156103dd576044879289928c916103c0575b508b83895196879586946364e329cb60e11b8652308c870152166024850152165af19081156103b6579086918991610389575b50169060065416176006558385541660604730895288865260c4858a20548860085416928751958694859363f305d71960e01b8552308a86015260248501528d60448501528d606485015260848401524260a48401525af1801561037f579084929161034c575b50604485600654169587541691888551978894859363095ea7b360e01b855284015260001960248401525af1908115610343575061030c575b5050805460ff60a01b1916600160a01b17905580f35b81813d831161033c575b6103208183610b8b565b8101031261033857518015150361011c5738806102f6565b8280fd5b503d610316565b513d86823e3d90fd5b6060809293503d8111610378575b6103648183610b8b565b81010312610374578290386102bd565b8580fd5b503d61035a565b83513d89823e3d90fd5b6103a99150863d88116103af575b6103a18183610b8b565b810190610e33565b38610256565b503d610397565b84513d8a823e3d90fd5b6103d79150843d86116103af576103a18183610b8b565b38610223565b85513d8b823e3d90fd5b6103ff919450823d84116103af576103a18183610b8b565b92386101fb565b845162461bcd60e51b81528085018790526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fd5b6020606492519162461bcd60e51b8352820152601760248201527f74726164696e6720697320616c7265616479206f70656e0000000000000000006044820152fd5b608490602084519162461bcd60e51b8352820152602160248201527f4f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f6044820152603760f91b6064820152fd5b9050346103385781600319360112610338576104fe610b5a565b9060243593303303610520575b602084610519878633610bc3565b5160018152f35b600594919454808302908382041483151715610562576127109004820391821161054f5750925080602061050b565b634e487b7160e01b815260118552602490fd5b634e487b7160e01b825260118652602482fd5b9050823461061a578260031936011261061a57610590610b5a565b918360243592338152600160205281812060018060a01b03861682526020522054908282106105c9576020856105198585038733610d31565b608490602086519162461bcd60e51b8352820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152fd5b80fd5b83833461011c578160031936011261011c57805191809380549160019083821c92828516948515610716575b6020958686108114610703578589529081156106df5750600114610687575b6106838787610679828c0383610b8b565b5191829182610b11565b0390f35b81529295507f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b8284106106cc57505050826106839461067992820101948680610668565b80548685018801529286019281016106ae565b60ff19168887015250505050151560051b8301019250610679826106838680610668565b634e487b7160e01b845260228352602484fd5b93607f1693610649565b50503461011c578160031936011261011c5760085490516001600160a01b039091168152602090f35b50503461011c578160031936011261011c576020906005549051908152f35b833461061a578060031936011261061a57600880546001600160a01b031916905580f35b50503461011c57602036600319011261011c5760209181906001600160a01b036107b4610b5a565b16815280845220549051908152f35b82843461061a578160031936011261061a576107dd610b5a565b338252600160209081528383206001600160a01b038316845290528282205460243581019290831061054f57602084610519858533610d31565b50503461011c578160031936011261011c576020905160128152f35b83833461011c57606036600319011261011c5761084e610b5a565b610856610b75565b6044359160018060a01b0381169485815260209560018752858220338352875285822054976000198903610893575b505050906105199291610bc3565b85891061096957811561091a5733156108cc5750948481979861051997845260018a528284203385528a52039120558594938780610885565b865162461bcd60e51b8152908101889052602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b865162461bcd60e51b81529081018890526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fd5b865162461bcd60e51b8152908101889052601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606490fd5b50503461011c578160031936011261011c5760209060ff60075460a01c1690519015158152f35b50503461011c578160031936011261011c576020906002549051908152f35b50503461011c578060031936011261011c57602090610519610a12610b5a565b6024359033610d31565b92915034610b0d5783600319360112610b0d57600354600181811c9186908281168015610b03575b6020958686108214610af05750848852908115610ace5750600114610a75575b6106838686610679828b0383610b8b565b929550600383527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b828410610abb575050508261068394610679928201019438610a64565b8054868501880152928601928101610a9e565b60ff191687860152505050151560051b83010192506106798261068338610a64565b634e487b7160e01b845260229052602483fd5b93607f1693610a44565b8380fd5b6020808252825181830181905290939260005b828110610b4657505060409293506000838284010152601f8019910116010190565b818101860151848201604001528501610b24565b600435906001600160a01b0382168203610b7057565b600080fd5b602435906001600160a01b0382168203610b7057565b90601f8019910116810190811067ffffffffffffffff821117610bad57604052565b634e487b7160e01b600052604160045260246000fd5b6001600160a01b03908116918215610cde5716918215610c8d57600082815280602052604081205491808310610c3957604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef958760209652828652038282205586815220818154019055604051908152a3565b60405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608490fd5b60405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608490fd5b60405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608490fd5b6001600160a01b03908116918215610de25716918215610d925760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925918360005260018252604060002085600052825280604060002055604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b60405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fd5b90816020910312610b7057516001600160a01b0381168103610b70579056fea2646970667358221220285c200b3978b10818ff576bb83f2dc4a2a7c98dfb6a36ea01170de792aa652764736f6c63430008140033000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000d3fd4f95820a9aa848ce716d6c200eaefb9a2e4900000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000003543131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035431310000000000000000000000000000000000000000000000000000000000c001a04e551c75810ffdfe6caff57da9f5a8732449f42f0f4c57f935b05250a76db3b6a046cd47e6d01914270c1ec0d9ac7fae7dfb240ec9a8b6ec7898c4d6aa174388f2";
1501
1502        let data = hex::decode(raw).unwrap();
1503        let tx = PooledTransactionVariant::decode_2718(&mut data.as_ref()).unwrap();
1504
1505        EthPooledTransaction::from_pooled(tx.try_into_recovered().unwrap())
1506    }
1507
1508    // <https://github.com/paradigmxyz/reth/issues/5178>
1509    #[tokio::test]
1510    async fn validate_transaction() {
1511        let transaction = get_transaction();
1512        let mut fork_tracker = ForkTracker {
1513            shanghai: false.into(),
1514            cancun: false.into(),
1515            prague: false.into(),
1516            osaka: false.into(),
1517            tip_timestamp: 0.into(),
1518            max_blob_count: 0.into(),
1519            max_initcode_size: AtomicUsize::new(MAX_INITCODE_SIZE),
1520            tx_gas_limit_cap: AtomicU64::new(0),
1521        };
1522
1523        let res = ensure_intrinsic_gas(&transaction, &fork_tracker);
1524        assert!(res.is_ok());
1525
1526        fork_tracker.shanghai = true.into();
1527        let res = ensure_intrinsic_gas(&transaction, &fork_tracker);
1528        assert!(res.is_ok());
1529
1530        let provider = MockEthProvider::default().with_genesis_block();
1531        provider.add_account(
1532            transaction.sender(),
1533            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1534        );
1535        let blob_store = InMemoryBlobStore::default();
1536        let validator = EthTransactionValidatorBuilder::new(provider, test_evm_config())
1537            .build(blob_store.clone());
1538
1539        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
1540
1541        assert!(outcome.is_valid());
1542
1543        let pool =
1544            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
1545
1546        let res = pool.add_external_transaction(transaction.clone()).await;
1547        assert!(res.is_ok());
1548        let tx = pool.get(transaction.hash());
1549        assert!(tx.is_some());
1550    }
1551
1552    // <https://github.com/paradigmxyz/reth/issues/8550>
1553    #[tokio::test]
1554    async fn invalid_on_gas_limit_too_high() {
1555        let transaction = get_transaction();
1556
1557        let provider = MockEthProvider::default().with_genesis_block();
1558        provider.add_account(
1559            transaction.sender(),
1560            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1561        );
1562
1563        let blob_store = InMemoryBlobStore::default();
1564        let validator = EthTransactionValidatorBuilder::new(provider, test_evm_config())
1565            .set_block_gas_limit(1_000_000) // tx gas limit is 1_015_288
1566            .build(blob_store.clone());
1567
1568        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
1569
1570        assert!(outcome.is_invalid());
1571
1572        let pool =
1573            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
1574
1575        let res = pool.add_external_transaction(transaction.clone()).await;
1576        assert!(res.is_err());
1577        assert!(matches!(
1578            res.unwrap_err().kind,
1579            PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::ExceedsGasLimit(
1580                1_015_288, 1_000_000
1581            ))
1582        ));
1583        let tx = pool.get(transaction.hash());
1584        assert!(tx.is_none());
1585    }
1586
1587    #[tokio::test]
1588    async fn invalid_on_fee_cap_exceeded() {
1589        let transaction = get_transaction();
1590        let provider = MockEthProvider::default().with_genesis_block();
1591        provider.add_account(
1592            transaction.sender(),
1593            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1594        );
1595
1596        let blob_store = InMemoryBlobStore::default();
1597        let validator = EthTransactionValidatorBuilder::new(provider, test_evm_config())
1598            .set_tx_fee_cap(100) // 100 wei cap
1599            .build(blob_store.clone());
1600
1601        let outcome = validator.validate_one(TransactionOrigin::Local, transaction.clone());
1602        assert!(outcome.is_invalid());
1603
1604        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
1605            assert!(matches!(
1606                err,
1607                InvalidPoolTransactionError::ExceedsFeeCap { max_tx_fee_wei, tx_fee_cap_wei }
1608                if (max_tx_fee_wei > tx_fee_cap_wei)
1609            ));
1610        }
1611
1612        let pool =
1613            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
1614        let res = pool.add_transaction(TransactionOrigin::Local, transaction.clone()).await;
1615        assert!(res.is_err());
1616        assert!(matches!(
1617            res.unwrap_err().kind,
1618            PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::ExceedsFeeCap { .. })
1619        ));
1620        let tx = pool.get(transaction.hash());
1621        assert!(tx.is_none());
1622    }
1623
1624    #[tokio::test]
1625    async fn valid_on_zero_fee_cap() {
1626        let transaction = get_transaction();
1627        let provider = MockEthProvider::default().with_genesis_block();
1628        provider.add_account(
1629            transaction.sender(),
1630            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1631        );
1632
1633        let blob_store = InMemoryBlobStore::default();
1634        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
1635            .set_tx_fee_cap(0) // no cap
1636            .build(blob_store);
1637
1638        let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
1639        assert!(outcome.is_valid());
1640    }
1641
1642    #[tokio::test]
1643    async fn valid_on_normal_fee_cap() {
1644        let transaction = get_transaction();
1645        let provider = MockEthProvider::default().with_genesis_block();
1646        provider.add_account(
1647            transaction.sender(),
1648            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1649        );
1650
1651        let blob_store = InMemoryBlobStore::default();
1652        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
1653            .set_tx_fee_cap(2e18 as u128) // 2 ETH cap
1654            .build(blob_store);
1655
1656        let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
1657        assert!(outcome.is_valid());
1658    }
1659
1660    #[tokio::test]
1661    async fn invalid_on_max_tx_gas_limit_exceeded() {
1662        let transaction = get_transaction();
1663        let provider = MockEthProvider::default().with_genesis_block();
1664        provider.add_account(
1665            transaction.sender(),
1666            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1667        );
1668
1669        let blob_store = InMemoryBlobStore::default();
1670        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
1671            .with_max_tx_gas_limit(Some(500_000)) // Set limit lower than transaction gas limit (1_015_288)
1672            .build(blob_store.clone());
1673
1674        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
1675        assert!(outcome.is_invalid());
1676
1677        let pool =
1678            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
1679
1680        let res = pool.add_external_transaction(transaction.clone()).await;
1681        assert!(res.is_err());
1682        assert!(matches!(
1683            res.unwrap_err().kind,
1684            PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::MaxTxGasLimitExceeded(
1685                1_015_288, 500_000
1686            ))
1687        ));
1688        let tx = pool.get(transaction.hash());
1689        assert!(tx.is_none());
1690    }
1691
1692    #[tokio::test]
1693    async fn valid_on_max_tx_gas_limit_disabled() {
1694        let transaction = get_transaction();
1695        let provider = MockEthProvider::default().with_genesis_block();
1696        provider.add_account(
1697            transaction.sender(),
1698            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1699        );
1700
1701        let blob_store = InMemoryBlobStore::default();
1702        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
1703            .with_max_tx_gas_limit(None) // disabled
1704            .build(blob_store);
1705
1706        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
1707        assert!(outcome.is_valid());
1708    }
1709
1710    #[tokio::test]
1711    async fn valid_on_max_tx_gas_limit_within_limit() {
1712        let transaction = get_transaction();
1713        let provider = MockEthProvider::default().with_genesis_block();
1714        provider.add_account(
1715            transaction.sender(),
1716            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1717        );
1718
1719        let blob_store = InMemoryBlobStore::default();
1720        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
1721            .with_max_tx_gas_limit(Some(2_000_000)) // Set limit higher than transaction gas limit (1_015_288)
1722            .build(blob_store);
1723
1724        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
1725        assert!(outcome.is_valid());
1726    }
1727
1728    // Helper function to set up common test infrastructure for priority fee tests
1729    fn setup_priority_fee_test() -> (EthPooledTransaction, MockEthProvider) {
1730        let transaction = get_transaction();
1731        let provider = MockEthProvider::default().with_genesis_block();
1732        provider.add_account(
1733            transaction.sender(),
1734            ExtendedAccount::new(transaction.nonce(), U256::MAX),
1735        );
1736        (transaction, provider)
1737    }
1738
1739    // Helper function to create a validator with minimum priority fee
1740    fn create_validator_with_minimum_fee(
1741        provider: MockEthProvider,
1742        minimum_priority_fee: Option<u128>,
1743        local_config: Option<LocalTransactionConfig>,
1744    ) -> EthTransactionValidator<MockEthProvider, EthPooledTransaction, EthEvmConfig> {
1745        let blob_store = InMemoryBlobStore::default();
1746        let mut builder = EthTransactionValidatorBuilder::new(provider, test_evm_config())
1747            .with_minimum_priority_fee(minimum_priority_fee);
1748
1749        if let Some(config) = local_config {
1750            builder = builder.with_local_transactions_config(config);
1751        }
1752
1753        builder.build(blob_store)
1754    }
1755
1756    #[tokio::test]
1757    async fn invalid_on_priority_fee_lower_than_configured_minimum() {
1758        let (transaction, provider) = setup_priority_fee_test();
1759
1760        // Verify the test transaction is a dynamic fee transaction
1761        assert!(transaction.is_dynamic_fee());
1762
1763        // Set minimum priority fee to be double the transaction's priority fee
1764        let minimum_priority_fee =
1765            transaction.max_priority_fee_per_gas().expect("priority fee is expected") * 2;
1766
1767        let validator =
1768            create_validator_with_minimum_fee(provider, Some(minimum_priority_fee), None);
1769
1770        // External transaction should be rejected due to low priority fee
1771        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
1772        assert!(outcome.is_invalid());
1773
1774        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
1775            assert!(matches!(
1776                err,
1777                InvalidPoolTransactionError::PriorityFeeBelowMinimum { minimum_priority_fee: min_fee }
1778                if min_fee == minimum_priority_fee
1779            ));
1780        }
1781
1782        // Test pool integration
1783        let blob_store = InMemoryBlobStore::default();
1784        let pool =
1785            Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
1786
1787        let res = pool.add_external_transaction(transaction.clone()).await;
1788        assert!(res.is_err());
1789        assert!(matches!(
1790            res.unwrap_err().kind,
1791            PoolErrorKind::InvalidTransaction(
1792                InvalidPoolTransactionError::PriorityFeeBelowMinimum { .. }
1793            )
1794        ));
1795        let tx = pool.get(transaction.hash());
1796        assert!(tx.is_none());
1797
1798        // Local transactions should still be accepted regardless of minimum priority fee
1799        let (_, local_provider) = setup_priority_fee_test();
1800        let validator_local =
1801            create_validator_with_minimum_fee(local_provider, Some(minimum_priority_fee), None);
1802
1803        let local_outcome = validator_local.validate_one(TransactionOrigin::Local, transaction);
1804        assert!(local_outcome.is_valid());
1805    }
1806
1807    #[tokio::test]
1808    async fn valid_on_priority_fee_equal_to_minimum() {
1809        let (transaction, provider) = setup_priority_fee_test();
1810
1811        // Set minimum priority fee equal to transaction's priority fee
1812        let tx_priority_fee =
1813            transaction.max_priority_fee_per_gas().expect("priority fee is expected");
1814        let validator = create_validator_with_minimum_fee(provider, Some(tx_priority_fee), None);
1815
1816        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
1817        assert!(outcome.is_valid());
1818    }
1819
1820    #[tokio::test]
1821    async fn valid_on_priority_fee_above_minimum() {
1822        let (transaction, provider) = setup_priority_fee_test();
1823
1824        // Set minimum priority fee below transaction's priority fee
1825        let tx_priority_fee =
1826            transaction.max_priority_fee_per_gas().expect("priority fee is expected");
1827        let minimum_priority_fee = tx_priority_fee / 2; // Half of transaction's priority fee
1828
1829        let validator =
1830            create_validator_with_minimum_fee(provider, Some(minimum_priority_fee), None);
1831
1832        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
1833        assert!(outcome.is_valid());
1834    }
1835
1836    #[tokio::test]
1837    async fn valid_on_minimum_priority_fee_disabled() {
1838        let (transaction, provider) = setup_priority_fee_test();
1839
1840        // No minimum priority fee set (default is None)
1841        let validator = create_validator_with_minimum_fee(provider, None, None);
1842
1843        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
1844        assert!(outcome.is_valid());
1845    }
1846
1847    #[tokio::test]
1848    async fn priority_fee_validation_applies_to_private_transactions() {
1849        let (transaction, provider) = setup_priority_fee_test();
1850
1851        // Set minimum priority fee to be double the transaction's priority fee
1852        let minimum_priority_fee =
1853            transaction.max_priority_fee_per_gas().expect("priority fee is expected") * 2;
1854
1855        let validator =
1856            create_validator_with_minimum_fee(provider, Some(minimum_priority_fee), None);
1857
1858        // Private transactions are also subject to minimum priority fee validation
1859        // because they are not considered "local" by default unless specifically configured
1860        let outcome = validator.validate_one(TransactionOrigin::Private, transaction);
1861        assert!(outcome.is_invalid());
1862
1863        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
1864            assert!(matches!(
1865                err,
1866                InvalidPoolTransactionError::PriorityFeeBelowMinimum { minimum_priority_fee: min_fee }
1867                if min_fee == minimum_priority_fee
1868            ));
1869        }
1870    }
1871
1872    #[tokio::test]
1873    async fn valid_on_local_config_exempts_private_transactions() {
1874        let (transaction, provider) = setup_priority_fee_test();
1875
1876        // Set minimum priority fee to be double the transaction's priority fee
1877        let minimum_priority_fee =
1878            transaction.max_priority_fee_per_gas().expect("priority fee is expected") * 2;
1879
1880        // Configure local transactions to include all private transactions
1881        let local_config =
1882            LocalTransactionConfig { propagate_local_transactions: true, ..Default::default() };
1883
1884        let validator = create_validator_with_minimum_fee(
1885            provider,
1886            Some(minimum_priority_fee),
1887            Some(local_config),
1888        );
1889
1890        // With appropriate local config, the behavior depends on the local transaction logic
1891        // This test documents the current behavior - private transactions are still validated
1892        // unless the sender is specifically whitelisted in local_transactions_config
1893        let outcome = validator.validate_one(TransactionOrigin::Private, transaction);
1894        assert!(outcome.is_invalid()); // Still invalid because sender not in whitelist
1895    }
1896
1897    #[test]
1898    fn reject_oversized_tx() {
1899        let mut transaction = get_transaction();
1900        transaction.encoded_length = DEFAULT_MAX_TX_INPUT_BYTES + 1;
1901        let provider = MockEthProvider::default().with_genesis_block();
1902
1903        // No minimum priority fee set (default is None)
1904        let validator = create_validator_with_minimum_fee(provider, None, None);
1905
1906        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
1907        let invalid = outcome.as_invalid().unwrap();
1908        assert!(invalid.is_oversized());
1909    }
1910
1911    #[tokio::test]
1912    async fn valid_with_disabled_balance_check() {
1913        let transaction = get_transaction();
1914        let provider = MockEthProvider::default().with_genesis_block();
1915
1916        // Set account with 0 balance
1917        provider.add_account(
1918            transaction.sender(),
1919            ExtendedAccount::new(transaction.nonce(), alloy_primitives::U256::ZERO),
1920        );
1921
1922        // Validate with balance check enabled
1923        let validator =
1924            EthTransactionValidatorBuilder::new(provider.clone(), EthEvmConfig::mainnet())
1925                .build(InMemoryBlobStore::default());
1926
1927        let outcome = validator.validate_one(TransactionOrigin::External, transaction.clone());
1928        let expected_cost = *transaction.cost();
1929        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
1930            assert!(matches!(
1931                err,
1932                InvalidPoolTransactionError::Consensus(InvalidTransactionError::InsufficientFunds(ref funds_err))
1933                if funds_err.got == alloy_primitives::U256::ZERO && funds_err.expected == expected_cost
1934            ));
1935        } else {
1936            panic!("Expected Invalid outcome with InsufficientFunds error");
1937        }
1938
1939        // Validate with balance check disabled
1940        let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
1941            .disable_balance_check()
1942            .build(InMemoryBlobStore::default());
1943
1944        let outcome = validator.validate_one(TransactionOrigin::External, transaction);
1945        assert!(outcome.is_valid()); // Should be valid because balance check is disabled
1946    }
1947}