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