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