Skip to main content

reth_transaction_pool/validate/
eth.rs

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