Skip to main content

tempo_transaction_pool/
validator.rs

1use crate::{
2    amm::AmmLiquidityCache,
3    state_cache::{StateCache, StateCacheDb},
4    transaction::{TempoPoolTransactionError, TempoPooledTransaction},
5};
6
7use alloy_consensus::constants::KECCAK_EMPTY;
8use alloy_evm::{Database, EvmEnv};
9use alloy_primitives::{Address, B256};
10use parking_lot::RwLock;
11use reth_chainspec::ChainSpecProvider;
12use reth_evm::ConfigureEvm;
13use reth_primitives_traits::{
14    Account, Bytecode, SealedBlock, transaction::error::InvalidTransactionError,
15};
16use reth_provider::BlockReaderIdExt;
17use reth_revm::database::StateProviderDatabase;
18use reth_storage_api::{
19    AccountReader, BytecodeReader, StateProvider, StateProviderBox, StateProviderFactory,
20    errors::{ProviderError, ProviderResult},
21};
22use reth_transaction_pool::{
23    EthTransactionValidator, PoolTransaction, TransactionOrigin, TransactionValidationOutcome,
24    TransactionValidator, error::InvalidPoolTransactionError,
25};
26use revm::{
27    DatabaseRef,
28    context::{
29        ContextTr, JournalTr,
30        result::{EVMError, InvalidTransaction},
31    },
32};
33use std::sync::{
34    Arc,
35    atomic::{AtomicU8, Ordering},
36};
37use tempo_chainspec::{
38    TempoChainSpec,
39    hardfork::{TempoHardfork, TempoHardforks},
40};
41use tempo_evm::{TempoEvmConfig, evm::TempoEvm};
42use tempo_precompiles::nonce::{INonce, NonceManager};
43use tempo_primitives::{
44    Block, TempoHeader,
45    subblock::has_sub_block_nonce_key_prefix,
46    transaction::{TEMPO_EXPIRING_NONCE_KEY, TempoTransaction},
47};
48use tempo_revm::{
49    TempoBlockEnv, TempoInvalidTransaction, TempoStateAccess, error::FeePaymentError,
50};
51
52// Reject AA txs where `valid_before` is too close to current time (or already expired) to prevent block invalidation.
53const AA_VALID_BEFORE_MIN_SECS: u64 = 3;
54
55/// Default maximum number of authorizations allowed in an AA transaction's authorization list.
56pub const DEFAULT_MAX_TEMPO_AUTHORIZATIONS: usize = 16;
57
58/// Maximum number of calls allowed per AA transaction (DoS protection).
59pub const MAX_AA_CALLS: usize = 32;
60
61/// Maximum size of input data per call in bytes (128KB, DoS protection).
62pub const MAX_CALL_INPUT_SIZE: usize = 128 * 1024;
63
64/// Maximum number of accounts in the access list (DoS protection).
65pub const MAX_ACCESS_LIST_ACCOUNTS: usize = 256;
66
67/// Maximum number of storage keys per account in the access list (DoS protection).
68pub const MAX_STORAGE_KEYS_PER_ACCOUNT: usize = 256;
69
70/// Maximum total number of storage keys across all accounts in the access list (DoS protection).
71pub const MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL: usize = 2048;
72
73/// Maximum number of token limits in a KeyAuthorization (DoS protection).
74pub const MAX_TOKEN_LIMITS: usize = 256;
75
76/// Default maximum allowed `valid_after` offset for AA txs (in seconds).
77///
78/// Aligned with the default queued transaction lifetime (`max_queued_lifetime = 120s`)
79/// so that transactions with a future `valid_after` are not silently evicted before
80/// they become executable.
81pub const DEFAULT_AA_VALID_AFTER_MAX_SECS: u64 = 120;
82
83/// Maximum number of call scopes per account key.
84const MAX_KEYCHAIN_CALL_SCOPES: u8 = 64;
85/// Maximum number of selector rules per call scope.
86const MAX_KEYCHAIN_SELECTOR_RULES_PER_SCOPE: u8 = 64;
87/// Maximum number of recipients per selector rule.
88const MAX_KEYCHAIN_RECIPIENTS_PER_SELECTOR: u8 = 64;
89
90/// Validator for Tempo transactions.
91#[derive(Debug)]
92pub struct TempoTransactionValidator<Client> {
93    /// Inner validator that performs default Ethereum tx validation.
94    pub(crate) inner: EthTransactionValidator<Client, TempoPooledTransaction, TempoEvmConfig>,
95    /// Maximum allowed `valid_after` offset for AA txs.
96    pub(crate) aa_valid_after_max_secs: u64,
97    /// Maximum number of authorizations allowed in an AA transaction.
98    pub(crate) max_tempo_authorizations: usize,
99    /// Cache of AMM liquidity for validator tokens.
100    pub(crate) amm_liquidity_cache: AmmLiquidityCache,
101    /// Cached EVM environment from the latest tip block, updated on each `on_new_head_block`.
102    cached_evm_env: RwLock<EvmEnv<TempoHardfork, TempoBlockEnv>>,
103    /// Tip hash and cache of state reads shared across validation calls, replaced on each
104    /// `on_new_head_block`.
105    cached_state: RwLock<(B256, Arc<StateCache>)>,
106    /// The Tempo hardfork active at the current tip, stored as an index into
107    /// [`TempoHardfork::VARIANTS`] and updated on each `on_new_head_block`.
108    ///
109    /// Cached here so hot paths can resolve the active hardfork with a single atomic load
110    /// instead of walking the chain spec's fork schedule.
111    active_hardfork: AtomicU8,
112}
113
114impl<Client> TempoTransactionValidator<Client>
115where
116    Client: ChainSpecProvider<ChainSpec = TempoChainSpec> + StateProviderFactory,
117{
118    pub fn new(
119        inner: EthTransactionValidator<Client, TempoPooledTransaction, TempoEvmConfig>,
120        aa_valid_after_max_secs: u64,
121        max_tempo_authorizations: usize,
122        amm_liquidity_cache: AmmLiquidityCache,
123    ) -> Self
124    where
125        Client: BlockReaderIdExt<Header = TempoHeader>,
126    {
127        let latest_header = inner
128            .client()
129            .latest_header()
130            .expect("failed to fetch latest header")
131            .expect("latest header is None");
132        let evm_env = inner
133            .evm_config()
134            .evm_env(latest_header.header())
135            .expect("failed constructing EvmEnv from latest header");
136        let active_hardfork = AtomicU8::new(evm_env.cfg_env.spec.variant_index());
137        Self {
138            inner,
139            aa_valid_after_max_secs,
140            max_tempo_authorizations,
141            amm_liquidity_cache,
142            cached_evm_env: parking_lot::RwLock::new(evm_env),
143            cached_state: RwLock::new((latest_header.hash(), Arc::new(StateCache::default()))),
144            active_hardfork,
145        }
146    }
147
148    /// Returns the Tempo hardfork active at the current tip.
149    ///
150    /// Updated on each `on_new_head_block`.
151    pub fn active_hardfork(&self) -> TempoHardfork {
152        TempoHardfork::from_variant_index(self.active_hardfork.load(Ordering::Relaxed))
153            .expect("stored hardfork index is valid")
154    }
155
156    /// Obtains a clone of the shared [`AmmLiquidityCache`].
157    pub fn amm_liquidity_cache(&self) -> AmmLiquidityCache {
158        self.amm_liquidity_cache.clone()
159    }
160
161    /// Returns the configured client
162    pub fn client(&self) -> &Client {
163        self.inner.client()
164    }
165
166    /// Pool-only time-bound admission checks.
167    ///
168    /// These enforce propagation-liveness constraints that are stricter than the EVM's
169    /// block-timestamp checks:
170    /// - `valid_before` must be far enough in the future (propagation buffer)
171    /// - `valid_after` must not be too far in the future (wall-clock bound)
172    fn ensure_pool_time_bounds(
173        &self,
174        tx: &TempoTransaction,
175    ) -> Result<(), TempoPoolTransactionError> {
176        let tip_timestamp = self.inner.fork_tracker().tip_timestamp();
177
178        // Reject AA txs where `valid_before` is too close to current time (or already expired).
179        // The EVM checks `valid_before > block_timestamp` but the pool needs an extra
180        // propagation buffer to prevent txs from expiring at peers with slightly newer tips.
181        if let Some(valid_before) = tx.valid_before {
182            let valid_before = valid_before.get();
183            let min_allowed = tip_timestamp.saturating_add(AA_VALID_BEFORE_MIN_SECS);
184            if valid_before <= min_allowed {
185                return Err(TempoPoolTransactionError::InvalidValidBefore {
186                    valid_before,
187                    min_allowed,
188                });
189            }
190        }
191
192        // Reject AA txs where `valid_after` is too far in the future.
193        // Uses wall-clock time to avoid rejecting valid txs when node is lagging.
194        if let Some(valid_after) = tx.valid_after {
195            let valid_after = valid_after.get();
196            let current_time = std::time::SystemTime::now()
197                .duration_since(std::time::UNIX_EPOCH)
198                .map(|d| d.as_secs())
199                .unwrap_or(0);
200            let max_allowed = current_time.saturating_add(self.aa_valid_after_max_secs);
201            if valid_after > max_allowed {
202                return Err(TempoPoolTransactionError::InvalidValidAfter {
203                    valid_after,
204                    max_allowed,
205                });
206            }
207        }
208
209        Ok(())
210    }
211
212    /// Validates that an AA transaction does not exceed the maximum authorization list size.
213    fn ensure_authorization_list_size(
214        &self,
215        transaction: &TempoPooledTransaction,
216    ) -> Result<(), TempoPoolTransactionError> {
217        let Some(aa_tx) = transaction.inner().as_aa() else {
218            return Ok(());
219        };
220
221        let count = aa_tx.tx().tempo_authorization_list.len();
222        if count > self.max_tempo_authorizations {
223            return Err(TempoPoolTransactionError::TooManyAuthorizations {
224                count,
225                max_allowed: self.max_tempo_authorizations,
226            });
227        }
228
229        Ok(())
230    }
231    /// Validates AA transaction field limits (calls, access list, token limits).
232    ///
233    /// These limits are enforced at the pool level rather than RLP decoding to:
234    /// - Keep the core transaction format flexible
235    /// - Allow peer penalization for sending bad transactions
236    fn ensure_aa_field_limits(
237        &self,
238        transaction: &TempoPooledTransaction,
239    ) -> Result<(), TempoPoolTransactionError> {
240        let Some(aa_tx) = transaction.inner().as_aa() else {
241            return Ok(());
242        };
243
244        let tx = aa_tx.tx();
245
246        // Check number of calls
247        if tx.calls.len() > MAX_AA_CALLS {
248            return Err(TempoPoolTransactionError::TooManyCalls {
249                count: tx.calls.len(),
250                max_allowed: MAX_AA_CALLS,
251            });
252        }
253
254        // Check each call's input size
255        for (idx, call) in tx.calls.iter().enumerate() {
256            if call.input.len() > MAX_CALL_INPUT_SIZE {
257                return Err(TempoPoolTransactionError::CallInputTooLarge {
258                    call_index: idx,
259                    size: call.input.len(),
260                    max_allowed: MAX_CALL_INPUT_SIZE,
261                });
262            }
263        }
264
265        // Check access list accounts
266        if tx.access_list.len() > MAX_ACCESS_LIST_ACCOUNTS {
267            return Err(TempoPoolTransactionError::TooManyAccessListAccounts {
268                count: tx.access_list.len(),
269                max_allowed: MAX_ACCESS_LIST_ACCOUNTS,
270            });
271        }
272
273        // Check storage keys per account and total
274        let mut total_storage_keys = 0usize;
275        for (idx, entry) in tx.access_list.iter().enumerate() {
276            if entry.storage_keys.len() > MAX_STORAGE_KEYS_PER_ACCOUNT {
277                return Err(TempoPoolTransactionError::TooManyStorageKeysPerAccount {
278                    account_index: idx,
279                    count: entry.storage_keys.len(),
280                    max_allowed: MAX_STORAGE_KEYS_PER_ACCOUNT,
281                });
282            }
283            total_storage_keys = total_storage_keys.saturating_add(entry.storage_keys.len());
284        }
285
286        if total_storage_keys > MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL {
287            return Err(TempoPoolTransactionError::TooManyTotalStorageKeys {
288                count: total_storage_keys,
289                max_allowed: MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL,
290            });
291        }
292
293        // Check key_authorization cardinality limits (DoS protection).
294        // Semantic validation (duplicates, zero-address, TIP-20, u128 cap) is handled by the
295        // EVM precompile via `validate_with_evm`.
296        if let Some(ref key_auth) = tx.key_authorization {
297            if let Some(limits) = &key_auth.limits
298                && limits.len() > MAX_TOKEN_LIMITS
299            {
300                return Err(TempoPoolTransactionError::TooManyTokenLimits {
301                    count: limits.len(),
302                    max_allowed: MAX_TOKEN_LIMITS,
303                });
304            }
305
306            if let Some(scopes) = &key_auth.allowed_calls {
307                if scopes.len() > MAX_KEYCHAIN_CALL_SCOPES as usize {
308                    return Err(TempoPoolTransactionError::Keychain(
309                        "too many call scopes in key authorization",
310                    ));
311                }
312
313                for scope in scopes {
314                    if scope.selector_rules.len() > MAX_KEYCHAIN_SELECTOR_RULES_PER_SCOPE as usize {
315                        return Err(TempoPoolTransactionError::Keychain(
316                            "too many selector rules in call scope",
317                        ));
318                    }
319
320                    for rule in &scope.selector_rules {
321                        if rule.recipients.len() > MAX_KEYCHAIN_RECIPIENTS_PER_SELECTOR as usize {
322                            return Err(TempoPoolTransactionError::Keychain(
323                                "too many recipients in selector rule",
324                            ));
325                        }
326                    }
327                }
328            }
329        }
330
331        Ok(())
332    }
333
334    /// Validates a batch of transactions against the same state snapshot.
335    ///
336    /// All transactions share one throwaway [`TempoEvm`] (journaled writes are discarded
337    /// after each transaction while loaded state stays warm) and the validator's tip-scoped
338    /// [`StateCache`], so repeated state reads are served from memory across transactions
339    /// and across concurrent validation calls.
340    fn validate_batch<P: StateProvider>(
341        &self,
342        state_provider: P,
343        cached_state: Arc<StateCache>,
344        transactions: impl IntoIterator<Item = (TransactionOrigin, TempoPooledTransaction)>,
345    ) -> Vec<TransactionValidationOutcome<TempoPooledTransaction>> {
346        let mut db = StateCacheDb::new(&cached_state, StateProviderDatabase::new(&state_provider));
347        let evm_env = self.cached_evm_env.read().clone();
348
349        // Create a throwaway EVM for the whole batch and run validation, reusing the same
350        // validation logic that the block executor uses ([`TempoEvm::validate_transaction`]).
351        // - Skip `valid_after` check: the pool intentionally accepts transactions with a
352        //   future `valid_after` (queued until executable).
353        // - Disable nonce check: the pool accepts future-nonce transactions (queued)
354        //   and handles nonce ordering separately.
355        // - Skip liquidity check: the pool performs its own liquidity validation against a cached view of the AMM state.
356        let mut evm = TempoEvm::new(&mut db, evm_env);
357        evm.inner_mut().skip_valid_after_check = true;
358        evm.inner_mut().skip_liquidity_check = true;
359        evm.ctx_mut().cfg.disable_nonce_check = true;
360
361        transactions
362            .into_iter()
363            .map(|(origin, transaction)| {
364                let outcome = self.validate_one_with_evm(origin, transaction, &mut evm);
365                // Discard this transaction's journaled writes (nonce bumps, fee deduction,
366                // key authorisation) while keeping loaded accounts and storage warm for the
367                // rest of the batch.
368                evm.ctx_mut().journal_mut().discard_tx();
369                outcome
370            })
371            .collect()
372    }
373
374    /// Returns the latest state provider and a state cache valid for the provider's tip.
375    fn latest_state_provider_and_cache(
376        &self,
377    ) -> ProviderResult<(StateProviderBox, Arc<StateCache>)> {
378        let state_provider = self.inner.client().latest()?;
379        let latest_hash = self.inner.client().chain_info()?.best_hash;
380        Ok((state_provider, self.state_cache_for_tip(latest_hash)))
381    }
382
383    /// Returns the shared cache if it matches `tip_hash`, otherwise an empty ephemeral cache.
384    ///
385    /// A mismatch can happen when `.latest()` observes state for a newer canonical tip before
386    /// `on_new_head_block` has refreshed the validator's cached state for that tip.
387    fn state_cache_for_tip(&self, tip_hash: B256) -> Arc<StateCache> {
388        let (cached_tip_hash, cached_state) = self.cached_state.read().clone();
389        if cached_tip_hash == tip_hash {
390            cached_state
391        } else {
392            Arc::new(StateCache::default())
393        }
394    }
395
396    /// Validates one transaction with the given throwaway EVM.
397    ///
398    /// The caller is responsible for discarding the journaled writes afterwards.
399    fn validate_one_with_evm<DB>(
400        &self,
401        origin: TransactionOrigin,
402        transaction: TempoPooledTransaction,
403        evm: &mut TempoEvm<DB>,
404    ) -> TransactionValidationOutcome<TempoPooledTransaction>
405    where
406        DB: Database<Error = ProviderError> + DatabaseRef<Error = ProviderError>,
407    {
408        // Get the hardfork active at the current tip
409        let spec = self.active_hardfork();
410
411        // Reject system transactions, those are never allowed in the pool.
412        if transaction.inner().is_system_tx() {
413            return TransactionValidationOutcome::Invalid(
414                transaction,
415                InvalidPoolTransactionError::Consensus(InvalidTransactionError::TxTypeNotSupported),
416            );
417        }
418
419        // Early reject oversized transactions before doing any expensive validation.
420        let tx_size = transaction.encoded_length();
421        let max_size = self.inner.max_tx_input_bytes();
422        if tx_size > max_size {
423            return TransactionValidationOutcome::Invalid(
424                transaction,
425                InvalidPoolTransactionError::OversizedData {
426                    size: tx_size,
427                    limit: max_size,
428                },
429            );
430        }
431
432        // Validate AA transaction authorization list size (pool-only DoS limit).
433        if let Err(err) = self.ensure_authorization_list_size(&transaction) {
434            return TransactionValidationOutcome::Invalid(
435                transaction,
436                InvalidPoolTransactionError::other(err),
437            );
438        }
439
440        // Validate AA transaction field limits (pool-only DoS limits: calls, access list, token limits).
441        if let Err(err) = self.ensure_aa_field_limits(&transaction) {
442            return TransactionValidationOutcome::Invalid(
443                transaction,
444                InvalidPoolTransactionError::other(err),
445            );
446        }
447
448        // Pool-only time-bound checks: valid_before propagation buffer, valid_after max offset.
449        if let Some(tx) = transaction.inner().as_aa()
450            && let Err(err) = self.ensure_pool_time_bounds(tx.tx())
451        {
452            return TransactionValidationOutcome::Invalid(
453                transaction,
454                InvalidPoolTransactionError::other(err),
455            );
456        }
457
458        // Run the unified EVM validation pipeline.
459        // This covers: non-zero value, keychain version, intrinsic gas, fee payer/token
460        // resolution & validation, nonce checks (protocol, 2D, expiring), keychain
461        // authorization, and balance checks.
462        //
463        // Returns resolved fee token and key expiry for pool caching.
464        let result = if let Some(tx_env) = transaction.cached_tx_env() {
465            evm.validate_transaction(tx_env.clone())
466        } else {
467            let result = evm.validate_transaction(transaction.tx_env_slow());
468            transaction.cache_tx_env(core::mem::take(&mut evm.ctx_mut().tx));
469            result
470        };
471        let validation_ctx = match result {
472            Ok(ctx) => ctx,
473            Err(err) => match err {
474                EVMError::Transaction(err) => {
475                    let err = match err {
476                        TempoInvalidTransaction::EthInvalidTransaction(
477                            InvalidTransaction::LackOfFundForMaxFee { fee, balance },
478                        ) => InvalidPoolTransactionError::Consensus(
479                            InvalidTransactionError::InsufficientFunds((*balance, *fee).into()),
480                        ),
481                        err => {
482                            InvalidPoolTransactionError::other(TempoPoolTransactionError::Evm(err))
483                        }
484                    };
485                    return TransactionValidationOutcome::Invalid(transaction, err);
486                }
487                other => {
488                    return TransactionValidationOutcome::Error(
489                        *transaction.hash(),
490                        Box::new(other),
491                    );
492                }
493            },
494        };
495
496        // Cache the resolved fee token from EVM validation for pool maintenance.
497        transaction.set_resolved_fee_token(validation_ctx.fee_token);
498
499        // Pool-only key-expiry propagation buffer: reject keychain txs whose key
500        // expires too soon (within AA_VALID_BEFORE_MIN_SECS of tip timestamp).
501        if let Some(key_expiry) = validation_ctx.key_expiry {
502            let min_allowed = self
503                .inner
504                .fork_tracker()
505                .tip_timestamp()
506                .saturating_add(AA_VALID_BEFORE_MIN_SECS);
507            if key_expiry <= min_allowed {
508                return TransactionValidationOutcome::Invalid(
509                    transaction,
510                    InvalidPoolTransactionError::other(
511                        TempoPoolTransactionError::AccessKeyExpired {
512                            expiry: key_expiry,
513                            min_allowed,
514                        },
515                    ),
516                );
517            }
518
519            // Cache the key expiry for pool maintenance eviction.
520            transaction.set_key_expiry(Some(key_expiry));
521        }
522
523        // Validate that transaction has enough liquidity against at least one of the recent validator tokens.
524        let fee = transaction.fee_token_cost();
525        match self.amm_liquidity_cache.has_enough_liquidity(
526            validation_ctx.fee_token,
527            fee,
528            evm.db_mut(),
529        ) {
530            Ok(true) => {}
531            Ok(false) => {
532                return TransactionValidationOutcome::Invalid(
533                    transaction,
534                    InvalidPoolTransactionError::other(TempoPoolTransactionError::Evm(
535                        TempoInvalidTransaction::CollectFeePreTx(
536                            FeePaymentError::InsufficientAmmLiquidity { fee },
537                        ),
538                    )),
539                );
540            }
541            Err(err) => {
542                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
543            }
544        }
545
546        // Delegate to the inner ETH validator for remaining checks
547        // (chain_id, EIP-3607 code check, protocol nonce, etc.) and to produce
548        // the Valid outcome with state_nonce and balance for pool ordering.
549        let inner_validation = {
550            let cached_state_provider = CachedAccountInfoReader::new(evm.db_ref());
551            self.inner
552                .validate_one_with_state_provider(origin, transaction, &cached_state_provider)
553        };
554
555        match inner_validation {
556            TransactionValidationOutcome::Valid {
557                balance,
558                mut state_nonce,
559                bytecode_hash,
560                transaction,
561                propagate,
562                authorities,
563            } => {
564                let mut authorities = authorities;
565                if let Some(aa_tx) = transaction.transaction().inner().as_aa() {
566                    let mut recovered_aa_authorities = aa_tx
567                        .tx()
568                        .tempo_authorization_list
569                        .iter()
570                        .filter_map(|authorization| authorization.recover_authority().ok())
571                        .collect::<Vec<_>>();
572
573                    if !recovered_aa_authorities.is_empty() {
574                        match authorities.as_mut() {
575                            Some(existing_authorities) => {
576                                existing_authorities.append(&mut recovered_aa_authorities)
577                            }
578                            None => authorities = Some(recovered_aa_authorities),
579                        }
580                    }
581                }
582
583                // Additional nonce validations for non-protocol nonce keys
584                if let Some(nonce_key) = transaction.transaction().nonce_key()
585                    && !nonce_key.is_zero()
586                {
587                    // ensure the nonce key isn't prefixed with the sub-block prefix
588                    if has_sub_block_nonce_key_prefix(&nonce_key) {
589                        return TransactionValidationOutcome::Invalid(
590                            transaction.into_transaction(),
591                            InvalidPoolTransactionError::other(
592                                TempoPoolTransactionError::SubblockNonceKey,
593                            ),
594                        );
595                    }
596
597                    // Check if T1 hardfork is active for expiring nonce handling
598                    let current_time = self.inner.fork_tracker().tip_timestamp();
599                    let is_t1_active = self
600                        .inner
601                        .chain_spec()
602                        .is_t1_active_at_timestamp(current_time);
603
604                    if is_t1_active && nonce_key == TEMPO_EXPIRING_NONCE_KEY {
605                        // Expiring nonce transactions are validated by the EVM
606                    } else {
607                        // This is a 2D nonce transaction - validate against 2D nonce
608                        state_nonce = match evm.db_mut().with_read_only_storage_ctx(spec, || {
609                            NonceManager::new().get_nonce(INonce::getNonceCall {
610                                account: transaction.transaction().sender(),
611                                nonceKey: nonce_key,
612                            })
613                        }) {
614                            Ok(nonce) => nonce,
615                            Err(err) => {
616                                return TransactionValidationOutcome::Error(
617                                    *transaction.hash(),
618                                    Box::new(err),
619                                );
620                            }
621                        };
622                        let tx_nonce = transaction.nonce();
623                        if tx_nonce < state_nonce {
624                            return TransactionValidationOutcome::Invalid(
625                                transaction.into_transaction(),
626                                InvalidTransactionError::NonceNotConsistent {
627                                    tx: tx_nonce,
628                                    state: state_nonce,
629                                }
630                                .into(),
631                            );
632                        }
633                    }
634                }
635
636                // Precompute the fee balance slot after validation has resolved the fee token.
637                transaction.transaction().fee_balance_slot();
638
639                // Precompute nonce storage slots for this transaction.
640                let _ = transaction.transaction().expiring_nonce_slot();
641                let _ = transaction.transaction().nonce_key_slot();
642
643                // Warm the global keccak cache with storage slot hashes for this transaction.
644                transaction.transaction().precalculate_keccak_slots();
645
646                TransactionValidationOutcome::Valid {
647                    balance,
648                    state_nonce,
649                    bytecode_hash,
650                    transaction,
651                    propagate,
652                    authorities,
653                }
654            }
655            outcome => outcome,
656        }
657    }
658}
659
660impl<Client> TransactionValidator for TempoTransactionValidator<Client>
661where
662    Client: ChainSpecProvider<ChainSpec = TempoChainSpec> + StateProviderFactory,
663{
664    type Transaction = TempoPooledTransaction;
665    type Block = Block;
666
667    async fn validate_transaction(
668        &self,
669        origin: TransactionOrigin,
670        transaction: Self::Transaction,
671    ) -> TransactionValidationOutcome<Self::Transaction> {
672        let (state_provider, cached_state) = match self.latest_state_provider_and_cache() {
673            Ok(provider_and_cache) => provider_and_cache,
674            Err(err) => {
675                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
676            }
677        };
678
679        self.validate_batch(
680            state_provider,
681            cached_state,
682            core::iter::once((origin, transaction)),
683        )
684        .pop()
685        .expect("validate_batch returns one outcome per transaction")
686    }
687
688    async fn validate_transactions(
689        &self,
690        transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction), IntoIter: Send>
691        + Send,
692    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
693        let (state_provider, cached_state) = match self.latest_state_provider_and_cache() {
694            Ok(provider_and_cache) => provider_and_cache,
695            Err(err) => {
696                return transactions
697                    .into_iter()
698                    .map(|(_, tx)| {
699                        TransactionValidationOutcome::Error(*tx.hash(), Box::new(err.clone()))
700                    })
701                    .collect();
702            }
703        };
704
705        self.validate_batch(state_provider, cached_state, transactions)
706    }
707
708    async fn validate_transactions_with_origin(
709        &self,
710        origin: TransactionOrigin,
711        transactions: impl IntoIterator<Item = Self::Transaction> + Send,
712    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
713        let (state_provider, cached_state) = match self.latest_state_provider_and_cache() {
714            Ok(provider_and_cache) => provider_and_cache,
715            Err(err) => {
716                return transactions
717                    .into_iter()
718                    .map(|tx| {
719                        TransactionValidationOutcome::Error(*tx.hash(), Box::new(err.clone()))
720                    })
721                    .collect();
722            }
723        };
724
725        self.validate_batch(
726            state_provider,
727            cached_state,
728            transactions.into_iter().map(|tx| (origin, tx)),
729        )
730    }
731
732    fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
733        self.inner.on_new_head_block(new_tip_block);
734
735        // Cache the EVM environment for the new tip block.
736        let evm_env = self
737            .inner
738            .evm_config()
739            .evm_env(new_tip_block.header())
740            .expect("invalid block in on_new_head_block");
741        self.active_hardfork
742            .store(evm_env.cfg_env.spec.variant_index(), Ordering::Relaxed);
743        *self.cached_evm_env.write() = evm_env;
744
745        // State changed, drop all cached reads and anchor the new cache to this tip.
746        *self.cached_state.write() = (new_tip_block.hash(), Arc::new(StateCache::default()));
747    }
748}
749
750/// Adapts a cached revm database back into the account info reader interface
751/// expected by the inner ETH transaction validator.
752struct CachedAccountInfoReader<DB> {
753    db: DB,
754}
755
756impl<DB> CachedAccountInfoReader<DB> {
757    const fn new(db: DB) -> Self {
758        Self { db }
759    }
760}
761
762impl<DB> AccountReader for CachedAccountInfoReader<DB>
763where
764    DB: DatabaseRef<Error = ProviderError>,
765{
766    fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
767        Ok(self.db.basic_ref(*address)?.map(|account| Account {
768            nonce: account.nonce,
769            balance: account.balance,
770            bytecode_hash: (account.code_hash != KECCAK_EMPTY).then_some(account.code_hash),
771        }))
772    }
773}
774
775impl<DB> BytecodeReader for CachedAccountInfoReader<DB>
776where
777    DB: DatabaseRef<Error = ProviderError>,
778{
779    fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
780        Ok(Some(Bytecode(self.db.code_by_hash_ref(*code_hash)?)))
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787    use crate::{test_utils::TxBuilder, transaction::TempoPoolTransactionError};
788    use alloy_consensus::{Header, Signed, Transaction, TxLegacy};
789    use alloy_primitives::{Address, B256, TxKind, U256, address, uint};
790    use alloy_signer::Signature;
791    use reth_chainspec::EthChainSpec;
792    use reth_primitives_traits::{Account, Bytecode, SignedTransaction};
793    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
794    use reth_revm::cached::CachedReads;
795    use reth_storage_api::{AccountReader, BlockNumReader, BytecodeReader};
796    use reth_transaction_pool::{
797        PoolTransaction, blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder,
798    };
799    use revm::{DatabaseRef, context::result::InvalidTransaction};
800    use std::sync::{
801        Arc,
802        atomic::{AtomicUsize, Ordering},
803    };
804    use tempo_chainspec::spec::{
805        MODERATO, TEMPO_T0_BASE_FEE, TEMPO_T1_BASE_FEE, TEMPO_T1_TX_GAS_LIMIT_CAP,
806    };
807    use tempo_precompiles::{
808        PATH_USD_ADDRESS,
809        tip20::{TIP20Token, slots as tip20_slots},
810    };
811    use tempo_primitives::{
812        Block, TempoHeader, TempoPrimitives, TempoTxEnvelope, TempoTxType,
813        transaction::{
814            TempoTransaction,
815            envelope::TEMPO_SYSTEM_TX_SIGNATURE,
816            tempo_transaction::Call,
817            tt_signature::{PrimitiveSignature, TempoSignature},
818            tt_signed::AASigned,
819        },
820    };
821
822    /// Arbitrary validity window (in seconds) used for expiring-nonce transactions in tests.
823    const TEST_VALIDITY_WINDOW: u64 = 25;
824
825    struct CountingDatabaseRef {
826        address: Address,
827        code_hash: B256,
828        account: revm::state::AccountInfo,
829        bytecode: revm::bytecode::Bytecode,
830        account_reads: Arc<AtomicUsize>,
831        bytecode_reads: Arc<AtomicUsize>,
832    }
833
834    impl DatabaseRef for CountingDatabaseRef {
835        type Error = ProviderError;
836
837        fn basic_ref(
838            &self,
839            address: Address,
840        ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
841            self.account_reads.fetch_add(1, Ordering::Relaxed);
842            Ok((address == self.address).then(|| self.account.clone()))
843        }
844
845        fn code_by_hash_ref(
846            &self,
847            code_hash: B256,
848        ) -> Result<revm::bytecode::Bytecode, Self::Error> {
849            self.bytecode_reads.fetch_add(1, Ordering::Relaxed);
850            Ok(if code_hash == self.code_hash {
851                self.bytecode.clone()
852            } else {
853                Default::default()
854            })
855        }
856
857        fn storage_ref(&self, _address: Address, _index: U256) -> Result<U256, Self::Error> {
858            Ok(U256::ZERO)
859        }
860
861        fn block_hash_ref(&self, _number: u64) -> Result<B256, Self::Error> {
862            Ok(B256::ZERO)
863        }
864    }
865
866    #[test]
867    fn cached_account_info_reader_uses_native_cached_reads() {
868        let address = Address::random();
869        let code_hash = B256::random();
870        let account = Account {
871            nonce: 7,
872            balance: U256::from(42),
873            bytecode_hash: Some(code_hash),
874        };
875        let bytecode = revm::bytecode::Bytecode::default();
876        let account_reads = Arc::new(AtomicUsize::new(0));
877        let bytecode_reads = Arc::new(AtomicUsize::new(0));
878        let provider = CountingDatabaseRef {
879            address,
880            code_hash,
881            account: revm::state::AccountInfo::new(
882                account.balance,
883                account.nonce,
884                code_hash,
885                bytecode.clone(),
886            ),
887            bytecode: bytecode.clone(),
888            account_reads: account_reads.clone(),
889            bytecode_reads: bytecode_reads.clone(),
890        };
891        let mut cached_reads = CachedReads::default();
892        let cached = CachedAccountInfoReader::new(cached_reads.as_db(provider));
893
894        assert_eq!(cached.basic_account(&address).unwrap(), Some(account));
895        assert_eq!(cached.basic_account(&address).unwrap(), Some(account));
896        assert_eq!(account_reads.load(Ordering::Relaxed), 1);
897
898        assert_eq!(
899            cached.bytecode_by_hash(&code_hash).unwrap(),
900            Some(Bytecode(bytecode.clone()))
901        );
902        assert_eq!(
903            cached.bytecode_by_hash(&code_hash).unwrap(),
904            Some(Bytecode(bytecode))
905        );
906        assert_eq!(bytecode_reads.load(Ordering::Relaxed), 1);
907    }
908
909    /// Helper to create a mock sealed block with the given timestamp.
910    fn create_mock_block(timestamp: u64) -> SealedBlock<Block> {
911        let header = TempoHeader {
912            inner: Header {
913                timestamp,
914                gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
915                excess_blob_gas: Some(0),
916                base_fee_per_gas: Some(TEMPO_T0_BASE_FEE),
917                ..Default::default()
918            },
919            ..Default::default()
920        };
921        let block = Block {
922            header,
923            body: Default::default(),
924        };
925        SealedBlock::seal_slow(block)
926    }
927
928    /// Helper function to create an AA transaction with the given `valid_after` and `valid_before`
929    /// timestamps
930    fn create_aa_transaction(
931        valid_after: Option<u64>,
932        valid_before: Option<u64>,
933    ) -> TempoPooledTransaction {
934        let mut builder = TxBuilder::aa(Address::random())
935            .fee_token(address!("0000000000000000000000000000000000000002"));
936        if let Some(va) = valid_after {
937            builder = builder.valid_after(va);
938        }
939        if let Some(vb) = valid_before {
940            builder = builder.valid_before(vb);
941        }
942        builder.build()
943    }
944
945    /// Helper function to setup validator with the given transaction and tip timestamp.
946    fn setup_validator(
947        transaction: &TempoPooledTransaction,
948        tip_timestamp: u64,
949    ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
950        let provider = MockEthProvider::<TempoPrimitives>::new()
951            .with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
952        provider.add_account(
953            transaction.sender(),
954            ExtendedAccount::new(transaction.nonce(), alloy_primitives::U256::ZERO),
955        );
956        let block_with_gas = Block {
957            header: TempoHeader {
958                inner: Header {
959                    gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
960                    ..Default::default()
961                },
962                ..Default::default()
963            },
964            ..Default::default()
965        };
966        provider.add_block(B256::random(), block_with_gas);
967
968        // Setup PATH_USD as a valid fee token with USD currency and always-allow transfer policy
969        // USD_CURRENCY_SLOT_VALUE: "USD" left-padded with length marker (3 bytes * 2 = 6)
970        let usd_currency_value =
971            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
972        // transfer_policy_id is packed at byte offset 20 in slot 7, so we need to shift
973        // policy_id=1 left by 160 bits (20 * 8) to position it correctly
974        let transfer_policy_id_packed =
975            uint!(0x0000000000000000000000010000000000000000000000000000000000000000_U256);
976        // Compute the balance slot for the sender in the PATH_USD token
977        let balance_slot = TIP20Token::from_address(PATH_USD_ADDRESS)
978            .expect("PATH_USD_ADDRESS is a valid TIP20 token")
979            .balances[transaction.sender()]
980        .slot();
981        // Give the sender enough balance to cover the transaction cost
982        let fee_payer_balance = U256::from(1_000_000_000_000u64); // 1M USD in 6 decimals
983        provider.add_account(
984            PATH_USD_ADDRESS,
985            ExtendedAccount::new(0, U256::ZERO).extend_storage([
986                (tip20_slots::CURRENCY.into(), usd_currency_value),
987                (
988                    tip20_slots::TRANSFER_POLICY_ID.into(),
989                    transfer_policy_id_packed,
990                ),
991                (balance_slot.into(), fee_payer_balance),
992            ]),
993        );
994
995        let inner =
996            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::moderato())
997                .with_custom_tx_type(TempoTxType::AA as u8)
998                .disable_balance_check()
999                .build(InMemoryBlobStore::default());
1000        let amm_cache =
1001            AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
1002        let validator = TempoTransactionValidator::new(
1003            inner,
1004            DEFAULT_AA_VALID_AFTER_MAX_SECS,
1005            DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1006            amm_cache,
1007        );
1008
1009        // Set the tip timestamp by simulating a new head block
1010        let mock_block = create_mock_block(tip_timestamp);
1011        validator.on_new_head_block(&mock_block);
1012
1013        validator
1014    }
1015
1016    #[test]
1017    fn state_cache_for_tip_reuses_only_matching_tip_cache() {
1018        let tx = TxBuilder::eip1559(Address::random()).build_eip1559();
1019        let validator = setup_validator(&tx, 1);
1020        let (shared_tip_hash, shared_cache) = validator.cached_state.read().clone();
1021
1022        let matching_cache = validator.state_cache_for_tip(shared_tip_hash);
1023        assert!(Arc::ptr_eq(&matching_cache, &shared_cache));
1024
1025        let mismatched_tip_hash = if shared_tip_hash == B256::repeat_byte(0x42) {
1026            B256::repeat_byte(0x43)
1027        } else {
1028            B256::repeat_byte(0x42)
1029        };
1030        let ephemeral_cache = validator.state_cache_for_tip(mismatched_tip_hash);
1031        assert!(!Arc::ptr_eq(&ephemeral_cache, &shared_cache));
1032    }
1033
1034    #[test]
1035    fn latest_state_provider_uses_ephemeral_cache_when_tip_hash_mismatches_latest() {
1036        let tx = TxBuilder::eip1559(Address::random()).build_eip1559();
1037        let validator = setup_validator(&tx, 1);
1038        let latest_hash = validator.client().chain_info().unwrap().best_hash;
1039        let mismatched_tip_hash = if latest_hash == B256::repeat_byte(0x42) {
1040            B256::repeat_byte(0x43)
1041        } else {
1042            B256::repeat_byte(0x42)
1043        };
1044        let shared_cache = Arc::new(StateCache::default());
1045        *validator.cached_state.write() = (mismatched_tip_hash, shared_cache.clone());
1046
1047        let (_, validation_cache) = validator.latest_state_provider_and_cache().unwrap();
1048
1049        assert!(!Arc::ptr_eq(&validation_cache, &shared_cache));
1050    }
1051
1052    #[tokio::test]
1053    async fn test_aa_authorization_list_authorities_tracked() {
1054        use alloy_eips::eip7702::Authorization;
1055        use alloy_signer::SignerSync;
1056        use alloy_signer_local::PrivateKeySigner;
1057        use tempo_primitives::transaction::{
1058            TempoSignedAuthorization,
1059            tt_signature::{PrimitiveSignature, TempoSignature},
1060        };
1061
1062        let current_time = std::time::SystemTime::now()
1063            .duration_since(std::time::UNIX_EPOCH)
1064            .unwrap()
1065            .as_secs();
1066
1067        let authority_signer = PrivateKeySigner::random();
1068        let expected_authority = authority_signer.address();
1069        let authorization = Authorization {
1070            chain_id: U256::from(1),
1071            nonce: 0,
1072            address: Address::random(),
1073        };
1074        let signature = authority_signer
1075            .sign_hash_sync(&authorization.signature_hash())
1076            .expect("authorization signing should succeed");
1077        let tempo_authorization = TempoSignedAuthorization::new_unchecked(
1078            authorization,
1079            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(signature)),
1080        );
1081
1082        let transaction = TxBuilder::aa(Address::random())
1083            .fee_token(PATH_USD_ADDRESS)
1084            .authorization_list(vec![tempo_authorization])
1085            .build();
1086        let validator = setup_validator(&transaction, current_time);
1087
1088        let outcome = validator
1089            .validate_transaction(TransactionOrigin::External, transaction)
1090            .await;
1091
1092        match outcome {
1093            TransactionValidationOutcome::Valid { authorities, .. } => {
1094                let authorities = authorities.expect(
1095                    "AA transactions with tempo_authorization_list should return authorities",
1096                );
1097                assert!(
1098                    authorities.contains(&expected_authority),
1099                    "AA authority recovered from tempo_authorization_list must be tracked"
1100                );
1101            }
1102            other => panic!("Expected Valid outcome with recovered authorities, got: {other:?}"),
1103        }
1104    }
1105
1106    #[tokio::test]
1107    async fn test_some_balance() {
1108        let transaction = TxBuilder::eip1559(Address::random())
1109            .value(U256::from(1))
1110            .build_eip1559();
1111        let validator = setup_validator(&transaction, 0);
1112
1113        let outcome = validator
1114            .validate_transaction(TransactionOrigin::External, transaction.clone())
1115            .await;
1116
1117        match outcome {
1118            TransactionValidationOutcome::Invalid(_, ref err) => {
1119                assert!(matches!(
1120                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1121                    Some(TempoPoolTransactionError::Evm(
1122                        TempoInvalidTransaction::ValueTransferNotAllowed
1123                    ))
1124                ));
1125            }
1126            _ => panic!("Expected Invalid outcome with Evm error, got: {outcome:?}"),
1127        }
1128    }
1129
1130    #[tokio::test]
1131    async fn test_system_tx_rejected_as_invalid() {
1132        let tx = TxLegacy {
1133            chain_id: Some(MODERATO.chain_id()),
1134            nonce: 0,
1135            gas_price: 0,
1136            gas_limit: 0,
1137            to: TxKind::Call(Address::ZERO),
1138            value: U256::ZERO,
1139            input: Default::default(),
1140        };
1141        let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1142        let transaction = TempoPooledTransaction::new(
1143            reth_primitives_traits::Recovered::new_unchecked(envelope, Address::ZERO),
1144        );
1145        let validator = setup_validator(&transaction, 0);
1146
1147        let outcome = validator
1148            .validate_transaction(TransactionOrigin::External, transaction)
1149            .await;
1150
1151        match outcome {
1152            TransactionValidationOutcome::Invalid(_, err) => {
1153                assert!(matches!(
1154                    err,
1155                    InvalidPoolTransactionError::Consensus(
1156                        InvalidTransactionError::TxTypeNotSupported
1157                    )
1158                ));
1159            }
1160            _ => panic!("Expected Invalid outcome with TxTypeNotSupported error, got: {outcome:?}"),
1161        }
1162    }
1163
1164    #[tokio::test]
1165    async fn test_invalid_fee_payer_signature_rejected() {
1166        let calls: Vec<Call> = vec![Call {
1167            to: TxKind::Call(Address::random()),
1168            value: U256::ZERO,
1169            input: Default::default(),
1170        }];
1171
1172        let tx = TempoTransaction {
1173            chain_id: MODERATO.chain_id(),
1174            max_priority_fee_per_gas: 1_000_000_000,
1175            max_fee_per_gas: 20_000_000_000,
1176            gas_limit: 1_000_000,
1177            calls,
1178            nonce_key: U256::ZERO,
1179            nonce: 0,
1180            fee_token: Some(PATH_USD_ADDRESS),
1181            fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
1182            ..Default::default()
1183        };
1184
1185        let signed = AASigned::new_unhashed(
1186            tx,
1187            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1188        );
1189        let transaction = TempoPooledTransaction::new(
1190            TempoTxEnvelope::from(signed).try_into_recovered().unwrap(),
1191        );
1192        let validator = setup_validator(&transaction, 0);
1193
1194        let outcome = validator
1195            .validate_transaction(TransactionOrigin::External, transaction)
1196            .await;
1197
1198        match outcome {
1199            TransactionValidationOutcome::Invalid(_, ref err) => {
1200                assert!(matches!(
1201                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1202                    Some(TempoPoolTransactionError::Evm(
1203                        TempoInvalidTransaction::InvalidFeePayerSignature
1204                    ))
1205                ));
1206            }
1207            _ => panic!("Expected Invalid outcome with Evm error, got: {outcome:?}"),
1208        }
1209    }
1210
1211    #[tokio::test]
1212    async fn test_self_sponsored_fee_payer_rejected() {
1213        use alloy_signer::SignerSync;
1214        use alloy_signer_local::PrivateKeySigner;
1215
1216        let signer = PrivateKeySigner::random();
1217        let sender = signer.address();
1218
1219        let mut tx = TempoTransaction {
1220            chain_id: MODERATO.chain_id(),
1221            max_priority_fee_per_gas: 1_000_000_000,
1222            max_fee_per_gas: 20_000_000_000,
1223            gas_limit: 1_000_000,
1224            calls: vec![Call {
1225                to: TxKind::Call(Address::random()),
1226                value: U256::ZERO,
1227                input: Default::default(),
1228            }],
1229            nonce_key: U256::ZERO,
1230            nonce: 0,
1231            fee_token: Some(PATH_USD_ADDRESS),
1232            fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
1233            ..Default::default()
1234        };
1235
1236        let fee_payer_hash = tx.fee_payer_signature_hash(sender);
1237        tx.fee_payer_signature = Some(
1238            signer
1239                .sign_hash_sync(&fee_payer_hash)
1240                .expect("fee payer signing should succeed"),
1241        );
1242
1243        let signed = AASigned::new_unhashed(
1244            tx,
1245            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1246        );
1247
1248        let envelope: TempoTxEnvelope = signed.into();
1249        let transaction = TempoPooledTransaction::new(
1250            reth_primitives_traits::Recovered::new_unchecked(envelope, sender),
1251        );
1252        let validator = setup_validator(&transaction, u64::MAX);
1253
1254        let outcome = validator
1255            .validate_transaction(TransactionOrigin::External, transaction)
1256            .await;
1257
1258        match outcome {
1259            TransactionValidationOutcome::Invalid(_, ref err) => {
1260                assert!(matches!(
1261                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1262                    Some(TempoPoolTransactionError::Evm(
1263                        TempoInvalidTransaction::SelfSponsoredFeePayer
1264                    ))
1265                ));
1266            }
1267            _ => panic!("Expected Invalid outcome with Evm error, got: {outcome:?}"),
1268        }
1269    }
1270
1271    #[tokio::test]
1272    async fn test_aa_valid_before_check() {
1273        // NOTE: `setup_validator` will turn `tip_timestamp` into `current_time`
1274        let current_time = std::time::SystemTime::now()
1275            .duration_since(std::time::UNIX_EPOCH)
1276            .unwrap()
1277            .as_secs();
1278
1279        // Test case 1: No `valid_before`
1280        let tx_no_valid_before = create_aa_transaction(None, None);
1281        let validator = setup_validator(&tx_no_valid_before, current_time);
1282        let outcome = validator
1283            .validate_transaction(TransactionOrigin::External, tx_no_valid_before)
1284            .await;
1285
1286        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1287            assert!(!matches!(
1288                err.downcast_other_ref::<TempoPoolTransactionError>(),
1289                Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1290            ));
1291        }
1292
1293        // Test case 2: `valid_before` too small (at boundary)
1294        let tx_too_close =
1295            create_aa_transaction(None, Some(current_time + AA_VALID_BEFORE_MIN_SECS));
1296        let validator = setup_validator(&tx_too_close, current_time);
1297        let outcome = validator
1298            .validate_transaction(TransactionOrigin::External, tx_too_close)
1299            .await;
1300
1301        match outcome {
1302            TransactionValidationOutcome::Invalid(_, ref err) => {
1303                assert!(matches!(
1304                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1305                    Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1306                ));
1307            }
1308            _ => panic!("Expected Invalid outcome with InvalidValidBefore error, got: {outcome:?}"),
1309        }
1310
1311        // Test case 3: `valid_before` sufficiently in the future
1312        let tx_valid =
1313            create_aa_transaction(None, Some(current_time + AA_VALID_BEFORE_MIN_SECS + 1));
1314        let validator = setup_validator(&tx_valid, current_time);
1315        let outcome = validator
1316            .validate_transaction(TransactionOrigin::External, tx_valid)
1317            .await;
1318
1319        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1320            assert!(!matches!(
1321                err.downcast_other_ref::<TempoPoolTransactionError>(),
1322                Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1323            ));
1324        }
1325    }
1326
1327    #[tokio::test]
1328    async fn test_aa_valid_after_check() {
1329        // NOTE: `setup_validator` will turn `tip_timestamp` into `current_time`
1330        let current_time = std::time::SystemTime::now()
1331            .duration_since(std::time::UNIX_EPOCH)
1332            .unwrap()
1333            .as_secs();
1334
1335        // Test case 1: No `valid_after`
1336        let tx_no_valid_after = create_aa_transaction(None, None);
1337        let validator = setup_validator(&tx_no_valid_after, current_time);
1338        let outcome = validator
1339            .validate_transaction(TransactionOrigin::External, tx_no_valid_after)
1340            .await;
1341
1342        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1343            assert!(!matches!(
1344                err.downcast_other_ref::<TempoPoolTransactionError>(),
1345                Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1346            ));
1347        }
1348
1349        // Test case 2: `valid_after` within limit (60 seconds)
1350        let tx_within_limit = create_aa_transaction(Some(current_time + 60), None);
1351        let validator = setup_validator(&tx_within_limit, current_time);
1352        let outcome = validator
1353            .validate_transaction(TransactionOrigin::External, tx_within_limit)
1354            .await;
1355
1356        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1357            assert!(!matches!(
1358                err.downcast_other_ref::<TempoPoolTransactionError>(),
1359                Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1360            ));
1361        }
1362
1363        // Test case 3: `valid_after` beyond limit (5 minutes, exceeds 120s max)
1364        let tx_too_far = create_aa_transaction(Some(current_time + 300), None);
1365        let validator = setup_validator(&tx_too_far, current_time);
1366        let outcome = validator
1367            .validate_transaction(TransactionOrigin::External, tx_too_far)
1368            .await;
1369
1370        match outcome {
1371            TransactionValidationOutcome::Invalid(_, ref err) => {
1372                assert!(matches!(
1373                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1374                    Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1375                ));
1376            }
1377            _ => panic!("Expected Invalid outcome with InvalidValidAfter error, got: {outcome:?}"),
1378        }
1379    }
1380
1381    /// Test AA intrinsic gas validation rejects insufficient gas and accepts sufficient gas.
1382    /// This is the fix for the audit finding about mempool DoS via gas calculation mismatch.
1383    #[tokio::test]
1384    async fn test_aa_intrinsic_gas_validation() {
1385        use alloy_primitives::{Signature, TxKind, address};
1386        use tempo_primitives::transaction::{
1387            TempoTransaction,
1388            tempo_transaction::Call,
1389            tt_signature::{PrimitiveSignature, TempoSignature},
1390            tt_signed::AASigned,
1391        };
1392
1393        let current_time = std::time::SystemTime::now()
1394            .duration_since(std::time::UNIX_EPOCH)
1395            .unwrap()
1396            .as_secs();
1397
1398        // Helper to create AA tx with given gas limit
1399        let create_aa_tx = |gas_limit: u64| {
1400            let calls: Vec<Call> = (0..10)
1401                .map(|i| Call {
1402                    to: TxKind::Call(Address::from([i as u8; 20])),
1403                    value: U256::ZERO,
1404                    input: alloy_primitives::Bytes::from(vec![0x00; 100]),
1405                })
1406                .collect();
1407
1408            let tx = TempoTransaction {
1409                chain_id: MODERATO.chain_id(),
1410                max_priority_fee_per_gas: 1_000_000_000,
1411                max_fee_per_gas: 20_000_000_000, // 20 gwei, above T1's minimum
1412                gas_limit,
1413                calls,
1414                nonce_key: U256::ZERO,
1415                nonce: 0,
1416                fee_token: Some(address!("0000000000000000000000000000000000000002")),
1417                ..Default::default()
1418            };
1419
1420            let signed = AASigned::new_unhashed(
1421                tx,
1422                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1423                    Signature::test_signature(),
1424                )),
1425            );
1426            TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1427        };
1428
1429        // Intrinsic gas for 10 calls: 21k base + 10*2600 cold access + 10*100*4 calldata = ~51k
1430        // Test 1: 30k gas should be rejected
1431        let tx_low_gas = create_aa_tx(30_000);
1432        let validator = setup_validator(&tx_low_gas, current_time);
1433        let outcome = validator
1434            .validate_transaction(TransactionOrigin::External, tx_low_gas)
1435            .await;
1436
1437        match outcome {
1438            TransactionValidationOutcome::Invalid(_, ref err) => {
1439                assert!(matches!(
1440                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1441                    Some(TempoPoolTransactionError::Evm(
1442                        TempoInvalidTransaction::EthInvalidTransaction(
1443                            InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1444                        )
1445                    ))
1446                ));
1447            }
1448            _ => panic!(
1449                "Expected Invalid outcome with InsufficientGasForAAIntrinsicCost, got: {outcome:?}"
1450            ),
1451        }
1452
1453        // Test 2: 1M gas should pass intrinsic gas check
1454        let tx_high_gas = create_aa_tx(1_000_000);
1455        let validator = setup_validator(&tx_high_gas, current_time);
1456        let outcome = validator
1457            .validate_transaction(TransactionOrigin::External, tx_high_gas)
1458            .await;
1459
1460        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1461            assert!(!matches!(
1462                err.downcast_other_ref::<TempoPoolTransactionError>(),
1463                Some(TempoPoolTransactionError::Evm(
1464                    TempoInvalidTransaction::EthInvalidTransaction(
1465                        InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1466                    )
1467                ))
1468            ));
1469        }
1470    }
1471
1472    /// Test that CREATE transactions with 2D nonce (nonce_key != 0) require additional gas
1473    /// when the sender's account nonce is 0 (account creation cost).
1474    ///
1475    /// The new logic adds 250k gas requirement when:
1476    /// - Transaction has 2D nonce (nonce_key != 0)
1477    /// - Transaction is CREATE
1478    /// - Account nonce is 0
1479    #[tokio::test]
1480    async fn test_aa_create_tx_with_2d_nonce_intrinsic_gas() {
1481        use alloy_primitives::Signature;
1482        use tempo_primitives::transaction::{
1483            TempoTransaction,
1484            tempo_transaction::Call as TxCall,
1485            tt_signature::{PrimitiveSignature, TempoSignature},
1486            tt_signed::AASigned,
1487        };
1488
1489        let current_time = std::time::SystemTime::now()
1490            .duration_since(std::time::UNIX_EPOCH)
1491            .unwrap()
1492            .as_secs();
1493
1494        // Helper to create AA transaction
1495        let create_aa_tx = |gas_limit: u64, nonce_key: U256, is_create: bool| {
1496            let calls: Vec<TxCall> = if is_create {
1497                vec![TxCall {
1498                    to: TxKind::Create,
1499                    value: U256::ZERO,
1500                    input: alloy_primitives::Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xF3]),
1501                }]
1502            } else {
1503                (0..10)
1504                    .map(|i| TxCall {
1505                        to: TxKind::Call(Address::from([i as u8; 20])),
1506                        value: U256::ZERO,
1507                        input: alloy_primitives::Bytes::from(vec![0x00; 100]),
1508                    })
1509                    .collect()
1510            };
1511
1512            let valid_before = if nonce_key == TEMPO_EXPIRING_NONCE_KEY {
1513                Some(core::num::NonZeroU64::new(current_time + TEST_VALIDITY_WINDOW).unwrap())
1514            } else {
1515                None
1516            };
1517
1518            let tx = TempoTransaction {
1519                chain_id: MODERATO.chain_id(),
1520                max_priority_fee_per_gas: 1_000_000_000,
1521                max_fee_per_gas: 20_000_000_000,
1522                gas_limit,
1523                calls,
1524                nonce_key,
1525                nonce: 0,
1526                valid_before,
1527                fee_token: Some(address!("0000000000000000000000000000000000000002")),
1528                ..Default::default()
1529            };
1530
1531            let signed = AASigned::new_unhashed(
1532                tx,
1533                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1534                    Signature::test_signature(),
1535                )),
1536            );
1537            TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1538        };
1539
1540        // Test 1: Verify 1D nonce (nonce_key=0) with low gas fails intrinsic gas check
1541        let tx_1d_low_gas = create_aa_tx(30_000, U256::ZERO, false);
1542        let validator1 = setup_validator(&tx_1d_low_gas, current_time);
1543        let outcome1 = validator1
1544            .validate_transaction(TransactionOrigin::External, tx_1d_low_gas)
1545            .await;
1546
1547        match outcome1 {
1548            TransactionValidationOutcome::Invalid(_, ref err) => {
1549                assert!(
1550                    matches!(
1551                        err.downcast_other_ref::<TempoPoolTransactionError>(),
1552                        Some(TempoPoolTransactionError::Evm(
1553                            TempoInvalidTransaction::EthInvalidTransaction(
1554                                InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1555                            )
1556                        ))
1557                    ),
1558                    "1D nonce with low gas should fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1559                );
1560            }
1561            _ => panic!("Expected Invalid outcome, got: {outcome1:?}"),
1562        }
1563
1564        // Test 2: Verify 2D nonce (nonce_key != 0) with same low gas also fails intrinsic gas check
1565        // This confirms that 2D nonce adds additional gas requirements (for nonce == 0 case)
1566        let tx_2d_low_gas = create_aa_tx(30_000, TEMPO_EXPIRING_NONCE_KEY, false);
1567        let validator2 = setup_validator(&tx_2d_low_gas, current_time);
1568        let outcome2 = validator2
1569            .validate_transaction(TransactionOrigin::External, tx_2d_low_gas)
1570            .await;
1571
1572        match outcome2 {
1573            TransactionValidationOutcome::Invalid(_, ref err) => {
1574                assert!(
1575                    matches!(
1576                        err.downcast_other_ref::<TempoPoolTransactionError>(),
1577                        Some(TempoPoolTransactionError::Evm(
1578                            TempoInvalidTransaction::EthInvalidTransaction(
1579                                InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1580                            )
1581                        ))
1582                    ),
1583                    "2D nonce with low gas should fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1584                );
1585            }
1586            _ => panic!("Expected Invalid outcome, got: {outcome2:?}"),
1587        }
1588
1589        // Test 3: 1D nonce with sufficient gas should NOT fail intrinsic gas check
1590        let tx_1d_high_gas = create_aa_tx(1_000_000, U256::ZERO, false);
1591        let validator3 = setup_validator(&tx_1d_high_gas, current_time);
1592        let outcome3 = validator3
1593            .validate_transaction(TransactionOrigin::External, tx_1d_high_gas)
1594            .await;
1595
1596        // May fail for other reasons (fee token, etc.) but should NOT fail intrinsic gas
1597        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome3 {
1598            assert!(
1599                !matches!(
1600                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1601                    Some(TempoPoolTransactionError::Evm(
1602                        TempoInvalidTransaction::EthInvalidTransaction(
1603                            InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1604                        )
1605                    ))
1606                ),
1607                "1D nonce with high gas should NOT fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1608            );
1609        }
1610
1611        // Test 4: 2D nonce with sufficient gas should NOT fail intrinsic gas check
1612        let tx_2d_high_gas = create_aa_tx(1_000_000, TEMPO_EXPIRING_NONCE_KEY, false);
1613        let validator4 = setup_validator(&tx_2d_high_gas, current_time);
1614        let outcome4 = validator4
1615            .validate_transaction(TransactionOrigin::External, tx_2d_high_gas)
1616            .await;
1617
1618        // May fail for other reasons (fee token, etc.) but should NOT fail intrinsic gas
1619        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome4 {
1620            assert!(
1621                !matches!(
1622                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1623                    Some(TempoPoolTransactionError::Evm(
1624                        TempoInvalidTransaction::EthInvalidTransaction(
1625                            InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1626                        )
1627                    ))
1628                ),
1629                "2D nonce with high gas should NOT fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1630            );
1631        }
1632    }
1633
1634    #[tokio::test]
1635    async fn test_expiring_nonce_intrinsic_gas_uses_lower_cost() {
1636        use alloy_primitives::{Signature, TxKind, address};
1637        use tempo_primitives::transaction::{
1638            TempoTransaction,
1639            tempo_transaction::Call,
1640            tt_signature::{PrimitiveSignature, TempoSignature},
1641            tt_signed::AASigned,
1642        };
1643
1644        let current_time = std::time::SystemTime::now()
1645            .duration_since(std::time::UNIX_EPOCH)
1646            .unwrap()
1647            .as_secs();
1648
1649        // Helper to create expiring nonce AA tx with given gas limit
1650        let create_expiring_nonce_tx = |gas_limit: u64| {
1651            let calls: Vec<Call> = vec![Call {
1652                to: TxKind::Call(Address::from([1u8; 20])),
1653                value: U256::ZERO,
1654                input: alloy_primitives::Bytes::from(vec![0xd0, 0x9d, 0xe0, 0x8a]), // increment()
1655            }];
1656
1657            let tx = TempoTransaction {
1658                chain_id: 1,
1659                max_priority_fee_per_gas: 1_000_000_000,
1660                max_fee_per_gas: 20_000_000_000,
1661                gas_limit,
1662                calls,
1663                nonce_key: TEMPO_EXPIRING_NONCE_KEY, // Expiring nonce
1664                nonce: 0,
1665                valid_before: Some(core::num::NonZeroU64::new(current_time + 25).unwrap()), // Valid for 25 seconds
1666                fee_token: Some(address!("0000000000000000000000000000000000000002")),
1667                ..Default::default()
1668            };
1669
1670            let signed = AASigned::new_unhashed(
1671                tx,
1672                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1673                    Signature::test_signature(),
1674                )),
1675            );
1676            TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1677        };
1678
1679        // Expiring nonce tx should only need ~35k gas (base + EXPIRING_NONCE_GAS of 13k)
1680        // NOT 250k+ which would be required for new account creation
1681        // Test: 50k gas should pass for expiring nonce (would fail if 250k was required)
1682        let tx = create_expiring_nonce_tx(50_000);
1683        let validator = setup_validator(&tx, current_time);
1684        let outcome = validator
1685            .validate_transaction(TransactionOrigin::External, tx)
1686            .await;
1687
1688        // Should NOT fail with InsufficientGasForAAIntrinsicCost or IntrinsicGasTooLow
1689        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1690            let is_intrinsic_gas_error = matches!(
1691                err.downcast_other_ref::<TempoPoolTransactionError>(),
1692                Some(TempoPoolTransactionError::Evm(
1693                    TempoInvalidTransaction::EthInvalidTransaction(
1694                        InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1695                    )
1696                ))
1697            ) || matches!(
1698                err.downcast_other_ref::<InvalidPoolTransactionError>(),
1699                Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
1700            );
1701            assert!(
1702                !is_intrinsic_gas_error,
1703                "Expiring nonce tx with 50k gas should NOT fail intrinsic gas check, got: {err:?}"
1704            );
1705        }
1706    }
1707
1708    /// Test that existing 2D nonce keys (nonce_key != 0 && nonce > 0) charge
1709    /// EXISTING_NONCE_KEY_GAS (5,000) during pool admission, matching handler.rs.
1710    ///
1711    /// Without this charge, transactions with a gas_limit 5,000 too low could
1712    /// pass pool validation but fail at execution time.
1713    #[tokio::test]
1714    async fn test_existing_2d_nonce_key_intrinsic_gas() {
1715        use alloy_primitives::{Signature, TxKind, address};
1716        use tempo_primitives::transaction::{
1717            TempoTransaction,
1718            tempo_transaction::Call,
1719            tt_signature::{PrimitiveSignature, TempoSignature},
1720            tt_signed::AASigned,
1721        };
1722
1723        let current_time = std::time::SystemTime::now()
1724            .duration_since(std::time::UNIX_EPOCH)
1725            .unwrap()
1726            .as_secs();
1727
1728        // Helper to create AA tx with a specific nonce_key and nonce
1729        let create_aa_tx = |gas_limit: u64, nonce_key: U256, nonce: u64| {
1730            let calls: Vec<Call> = vec![Call {
1731                to: TxKind::Call(Address::from([1u8; 20])),
1732                value: U256::ZERO,
1733                input: alloy_primitives::Bytes::from(vec![0xd0, 0x9d, 0xe0, 0x8a]), // increment()
1734            }];
1735
1736            let tx = TempoTransaction {
1737                chain_id: MODERATO.chain_id(),
1738                max_priority_fee_per_gas: 1_000_000_000,
1739                max_fee_per_gas: 20_000_000_000,
1740                gas_limit,
1741                calls,
1742                nonce_key,
1743                nonce,
1744                fee_token: Some(address!("0000000000000000000000000000000000000002")),
1745                ..Default::default()
1746            };
1747
1748            let signed = AASigned::new_unhashed(
1749                tx,
1750                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1751                    Signature::test_signature(),
1752                )),
1753            );
1754            TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1755        };
1756
1757        // Test 1: 1D nonce (nonce_key=0) with nonce > 0 has no extra 2D nonce charge.
1758        // 50k gas should be sufficient (base ~21k + calldata).
1759        let tx_1d = create_aa_tx(50_000, U256::ZERO, 5);
1760        let validator = setup_validator(&tx_1d, current_time);
1761        let outcome = validator
1762            .validate_transaction(TransactionOrigin::External, tx_1d)
1763            .await;
1764
1765        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1766            let is_gas_error = matches!(
1767                err.downcast_other_ref::<TempoPoolTransactionError>(),
1768                Some(TempoPoolTransactionError::Evm(
1769                    TempoInvalidTransaction::EthInvalidTransaction(
1770                        InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1771                    )
1772                ))
1773            ) || matches!(
1774                err.downcast_other_ref::<InvalidPoolTransactionError>(),
1775                Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
1776            );
1777            assert!(
1778                !is_gas_error,
1779                "1D nonce with nonce>0 and 50k gas should NOT fail intrinsic gas check, got: {err:?}"
1780            );
1781        }
1782
1783        // Test 2: 2D nonce (nonce_key != 0) with nonce > 0, same 50k gas.
1784        // This triggers the EXISTING_NONCE_KEY_GAS branch (+5k), but 50k is still enough.
1785        let tx_2d_ok = create_aa_tx(50_000, U256::from(1), 5);
1786        let validator = setup_validator(&tx_2d_ok, current_time);
1787        let outcome = validator
1788            .validate_transaction(TransactionOrigin::External, tx_2d_ok)
1789            .await;
1790
1791        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1792            let is_gas_error = matches!(
1793                err.downcast_other_ref::<TempoPoolTransactionError>(),
1794                Some(TempoPoolTransactionError::Evm(
1795                    TempoInvalidTransaction::EthInvalidTransaction(
1796                        InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1797                    )
1798                ))
1799            ) || matches!(
1800                err.downcast_other_ref::<InvalidPoolTransactionError>(),
1801                Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
1802            );
1803            assert!(
1804                !is_gas_error,
1805                "Existing 2D nonce key with 50k gas should NOT fail intrinsic gas check, got: {err:?}"
1806            );
1807        }
1808
1809        // Test 3: 2D nonce (nonce_key != 0), nonce > 0, with gas that is sufficient for
1810        // base intrinsic gas but NOT sufficient when EXISTING_NONCE_KEY_GAS (5k) is added.
1811        // Use 22_000 gas: enough for base ~21k + calldata but not when +5k is charged.
1812        let tx_2d_low = create_aa_tx(22_000, U256::from(1), 5);
1813        let validator = setup_validator(&tx_2d_low, current_time);
1814        let outcome = validator
1815            .validate_transaction(TransactionOrigin::External, tx_2d_low)
1816            .await;
1817
1818        match outcome {
1819            TransactionValidationOutcome::Invalid(_, ref err) => {
1820                let is_gas_error = matches!(
1821                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1822                    Some(TempoPoolTransactionError::Evm(
1823                        TempoInvalidTransaction::EthInvalidTransaction(
1824                            InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1825                        )
1826                    ))
1827                ) || matches!(
1828                    err.downcast_other_ref::<InvalidPoolTransactionError>(),
1829                    Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
1830                );
1831                assert!(
1832                    is_gas_error,
1833                    "Existing 2D nonce key with 22k gas should fail intrinsic gas check, got: {err:?}"
1834                );
1835            }
1836            _ => panic!(
1837                "Expected Invalid outcome for existing 2D nonce with insufficient gas, got: {outcome:?}"
1838            ),
1839        }
1840
1841        // Test 4: Same scenario as test 3, but with 1D nonce (nonce_key=0).
1842        // Without the 5k charge, 22k should be sufficient.
1843        let tx_1d_low = create_aa_tx(22_000, U256::ZERO, 5);
1844        let validator = setup_validator(&tx_1d_low, current_time);
1845        let outcome = validator
1846            .validate_transaction(TransactionOrigin::External, tx_1d_low)
1847            .await;
1848
1849        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1850            let is_gas_error = matches!(
1851                err.downcast_other_ref::<TempoPoolTransactionError>(),
1852                Some(TempoPoolTransactionError::Evm(
1853                    TempoInvalidTransaction::EthInvalidTransaction(
1854                        InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
1855                    )
1856                ))
1857            ) || matches!(
1858                err.downcast_other_ref::<InvalidPoolTransactionError>(),
1859                Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
1860            );
1861            assert!(
1862                !is_gas_error,
1863                "1D nonce with nonce>0 and 22k gas should NOT fail intrinsic gas check, got: {err:?}"
1864            );
1865        }
1866    }
1867
1868    #[tokio::test]
1869    async fn test_non_zero_value_in_eip1559_rejected() {
1870        let transaction = TxBuilder::eip1559(Address::random())
1871            .value(U256::from(1))
1872            .build_eip1559();
1873
1874        let current_time = std::time::SystemTime::now()
1875            .duration_since(std::time::UNIX_EPOCH)
1876            .unwrap()
1877            .as_secs();
1878        let validator = setup_validator(&transaction, current_time);
1879
1880        let outcome = validator
1881            .validate_transaction(TransactionOrigin::External, transaction)
1882            .await;
1883
1884        match outcome {
1885            TransactionValidationOutcome::Invalid(_, ref err) => {
1886                assert!(matches!(
1887                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1888                    Some(TempoPoolTransactionError::Evm(
1889                        TempoInvalidTransaction::ValueTransferNotAllowed
1890                    ))
1891                ));
1892            }
1893            _ => panic!("Expected Invalid outcome with Evm error, got: {outcome:?}"),
1894        }
1895    }
1896
1897    #[tokio::test]
1898    async fn test_zero_value_passes_value_check() {
1899        // Create a zero-value EIP-1559 transaction (value defaults to 0 in TxBuilder)
1900        let transaction = TxBuilder::eip1559(Address::random()).build_eip1559();
1901        assert!(transaction.value().is_zero(), "Test expects zero-value tx");
1902
1903        let current_time = std::time::SystemTime::now()
1904            .duration_since(std::time::UNIX_EPOCH)
1905            .unwrap()
1906            .as_secs();
1907        let validator = setup_validator(&transaction, current_time);
1908
1909        let outcome = validator
1910            .validate_transaction(TransactionOrigin::External, transaction)
1911            .await;
1912
1913        assert!(
1914            matches!(outcome, TransactionValidationOutcome::Valid { .. }),
1915            "Zero-value tx should pass validation, got: {outcome:?}"
1916        );
1917    }
1918
1919    #[tokio::test]
1920    async fn test_invalid_fee_token_rejected() {
1921        let invalid_fee_token = address!("1234567890123456789012345678901234567890");
1922
1923        let transaction = TxBuilder::aa(Address::random())
1924            .fee_token(invalid_fee_token)
1925            .build();
1926
1927        let current_time = std::time::SystemTime::now()
1928            .duration_since(std::time::UNIX_EPOCH)
1929            .unwrap()
1930            .as_secs();
1931        let validator = setup_validator(&transaction, current_time);
1932
1933        let outcome = validator
1934            .validate_transaction(TransactionOrigin::External, transaction)
1935            .await;
1936
1937        match outcome {
1938            TransactionValidationOutcome::Invalid(_, ref err) => {
1939                assert!(matches!(
1940                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1941                    Some(TempoPoolTransactionError::Evm(
1942                        TempoInvalidTransaction::FeeTokenNotTip20 { .. }
1943                    ))
1944                ));
1945            }
1946            _ => panic!("Expected Invalid outcome with Evm error, got: {outcome:?}"),
1947        }
1948    }
1949
1950    #[tokio::test]
1951    async fn test_aa_valid_after_and_valid_before_both_valid() {
1952        let current_time = std::time::SystemTime::now()
1953            .duration_since(std::time::UNIX_EPOCH)
1954            .unwrap()
1955            .as_secs();
1956
1957        let valid_after = current_time + 60;
1958        let valid_before = current_time + 3600;
1959
1960        let transaction = create_aa_transaction(Some(valid_after), Some(valid_before));
1961        let validator = setup_validator(&transaction, current_time);
1962
1963        let outcome = validator
1964            .validate_transaction(TransactionOrigin::External, transaction)
1965            .await;
1966
1967        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1968            let tempo_err = err.downcast_other_ref::<TempoPoolTransactionError>();
1969            assert!(
1970                !matches!(
1971                    tempo_err,
1972                    Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1973                        | Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1974                ),
1975                "Should not fail with validity window errors"
1976            );
1977        }
1978    }
1979
1980    #[tokio::test]
1981    async fn test_fee_cap_below_min_base_fee_rejected() {
1982        let current_time = std::time::SystemTime::now()
1983            .duration_since(std::time::UNIX_EPOCH)
1984            .unwrap()
1985            .as_secs();
1986
1987        // T0 base fee is 10 gwei (10_000_000_000 wei)
1988        // Create a transaction with max_fee_per_gas below this
1989        let transaction = TxBuilder::aa(Address::random())
1990            .max_fee(1_000_000_000) // 1 gwei, below T0's 10 gwei
1991            .max_priority_fee(1_000_000_000)
1992            .build();
1993
1994        let validator = setup_validator(&transaction, current_time);
1995
1996        let outcome = validator
1997            .validate_transaction(TransactionOrigin::External, transaction)
1998            .await;
1999
2000        match outcome {
2001            TransactionValidationOutcome::Invalid(_, ref err) => {
2002                assert!(
2003                    matches!(
2004                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2005                        Some(TempoPoolTransactionError::Evm(
2006                            TempoInvalidTransaction::EthInvalidTransaction(
2007                                InvalidTransaction::GasPriceLessThanBasefee
2008                            )
2009                        ))
2010                    ),
2011                    "Expected Evm error, got: {err:?}"
2012                );
2013            }
2014            _ => panic!("Expected Invalid outcome with Evm error, got: {outcome:?}"),
2015        }
2016    }
2017
2018    #[tokio::test]
2019    async fn test_fee_cap_at_min_base_fee_passes() {
2020        let current_time = std::time::SystemTime::now()
2021            .duration_since(std::time::UNIX_EPOCH)
2022            .unwrap()
2023            .as_secs();
2024
2025        // Create a transaction with max_fee_per_gas exactly at the fixed T1+ minimum.
2026        let transaction = TxBuilder::aa(Address::random())
2027            .max_fee(u128::from(TEMPO_T1_BASE_FEE))
2028            .max_priority_fee(1_000_000_000)
2029            .build();
2030
2031        let validator = setup_validator(&transaction, current_time);
2032
2033        let outcome = validator
2034            .validate_transaction(TransactionOrigin::External, transaction)
2035            .await;
2036
2037        // Should not fail with FeeCapBelowMinBaseFee
2038        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2039            assert!(
2040                !matches!(
2041                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2042                    Some(TempoPoolTransactionError::Evm(
2043                        TempoInvalidTransaction::EthInvalidTransaction(
2044                            InvalidTransaction::GasPriceLessThanBasefee
2045                        )
2046                    ))
2047                ),
2048                "Should not fail with FeeCapBelowMinBaseFee when fee cap equals min base fee"
2049            );
2050        }
2051    }
2052
2053    #[tokio::test]
2054    async fn test_fee_cap_above_min_base_fee_passes() {
2055        let current_time = std::time::SystemTime::now()
2056            .duration_since(std::time::UNIX_EPOCH)
2057            .unwrap()
2058            .as_secs();
2059
2060        // T0 base fee is 10 gwei (10_000_000_000 wei)
2061        // Create a transaction with max_fee_per_gas above minimum
2062        let transaction = TxBuilder::aa(Address::random())
2063            .max_fee(20_000_000_000) // 20 gwei, above T0's 10 gwei
2064            .max_priority_fee(1_000_000_000)
2065            .build();
2066
2067        let validator = setup_validator(&transaction, current_time);
2068
2069        let outcome = validator
2070            .validate_transaction(TransactionOrigin::External, transaction)
2071            .await;
2072
2073        // Should not fail with FeeCapBelowMinBaseFee
2074        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2075            assert!(
2076                !matches!(
2077                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2078                    Some(TempoPoolTransactionError::Evm(
2079                        TempoInvalidTransaction::EthInvalidTransaction(
2080                            InvalidTransaction::GasPriceLessThanBasefee
2081                        )
2082                    ))
2083                ),
2084                "Should not fail with FeeCapBelowMinBaseFee when fee cap is above min base fee"
2085            );
2086        }
2087    }
2088
2089    #[tokio::test]
2090    async fn test_eip1559_fee_cap_below_min_base_fee_rejected() {
2091        let current_time = std::time::SystemTime::now()
2092            .duration_since(std::time::UNIX_EPOCH)
2093            .unwrap()
2094            .as_secs();
2095
2096        // T0 base fee is 10 gwei, create EIP-1559 tx with lower fee
2097        let transaction = TxBuilder::eip1559(Address::random())
2098            .max_fee(1_000_000_000) // 1 gwei, below T0's 10 gwei
2099            .max_priority_fee(1_000_000_000)
2100            .build_eip1559();
2101
2102        let validator = setup_validator(&transaction, current_time);
2103
2104        let outcome = validator
2105            .validate_transaction(TransactionOrigin::External, transaction)
2106            .await;
2107
2108        match outcome {
2109            TransactionValidationOutcome::Invalid(_, ref err) => {
2110                assert!(
2111                    matches!(
2112                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2113                        Some(TempoPoolTransactionError::Evm(
2114                            TempoInvalidTransaction::EthInvalidTransaction(
2115                                InvalidTransaction::GasPriceLessThanBasefee
2116                            )
2117                        ))
2118                    ),
2119                    "Expected Evm error for EIP-1559 tx, got: {err:?}"
2120                );
2121            }
2122            _ => panic!("Expected Invalid outcome with Evm error, got: {outcome:?}"),
2123        }
2124    }
2125
2126    mod keychain_validation {
2127        use super::*;
2128        use reth_transaction_pool::error::PoolTransactionError;
2129
2130        #[test]
2131        fn test_legacy_keychain_post_t1c_is_bad_transaction() {
2132            assert!(
2133                TempoPoolTransactionError::Evm(TempoInvalidTransaction::LegacyKeychainSignature)
2134                    .is_bad_transaction(),
2135                "Post-T1C V1 rejection should be a bad transaction (permanent)"
2136            );
2137        }
2138
2139        #[test]
2140        fn test_v2_keychain_pre_t1c_is_not_bad_transaction() {
2141            assert!(
2142                !TempoPoolTransactionError::Evm(
2143                    TempoInvalidTransaction::V2KeychainBeforeActivation
2144                )
2145                .is_bad_transaction(),
2146                "Pre-T1C V2 rejection should NOT be a bad transaction (transient)"
2147            );
2148        }
2149
2150        #[test]
2151        fn test_expired_access_key_is_not_bad_transaction() {
2152            assert!(
2153                !TempoPoolTransactionError::AccessKeyExpired {
2154                    expiry: 1,
2155                    min_allowed: 4,
2156                }
2157                .is_bad_transaction(),
2158                "Expired access key rejection should NOT be a bad transaction (timing-sensitive)"
2159            );
2160        }
2161
2162        #[test]
2163        fn test_expired_key_authorization_is_not_bad_transaction() {
2164            assert!(
2165                !TempoPoolTransactionError::KeyAuthorizationExpired {
2166                    expiry: 1,
2167                    min_allowed: 4,
2168                }
2169                .is_bad_transaction(),
2170                "Expired key authorization rejection should NOT be a bad transaction (timing-sensitive)"
2171            );
2172        }
2173    }
2174
2175    // ============================================
2176    // Authorization list limit tests
2177    // ============================================
2178
2179    /// Helper function to create an AA transaction with the given number of authorizations.
2180    fn create_aa_transaction_with_authorizations(
2181        authorization_count: usize,
2182    ) -> TempoPooledTransaction {
2183        use alloy_eips::eip7702::Authorization;
2184        use alloy_primitives::{Signature, TxKind, address};
2185        use tempo_primitives::transaction::{
2186            TempoSignedAuthorization, TempoTransaction,
2187            tempo_transaction::Call,
2188            tt_signature::{PrimitiveSignature, TempoSignature},
2189            tt_signed::AASigned,
2190        };
2191
2192        // Create dummy authorizations
2193        let authorizations: Vec<TempoSignedAuthorization> = (0..authorization_count)
2194            .map(|i| {
2195                let auth = Authorization {
2196                    chain_id: U256::from(1),
2197                    nonce: i as u64,
2198                    address: address!("0000000000000000000000000000000000000001"),
2199                };
2200                TempoSignedAuthorization::new_unchecked(
2201                    auth,
2202                    TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2203                        Signature::test_signature(),
2204                    )),
2205                )
2206            })
2207            .collect();
2208
2209        let tx_aa = TempoTransaction {
2210            chain_id: 1,
2211            max_priority_fee_per_gas: 1_000_000_000,
2212            max_fee_per_gas: 20_000_000_000, // 20 gwei, above T1's minimum
2213            gas_limit: 1_000_000,
2214            calls: vec![Call {
2215                to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
2216                value: U256::ZERO,
2217                input: alloy_primitives::Bytes::new(),
2218            }],
2219            nonce_key: U256::ZERO,
2220            nonce: 0,
2221            fee_token: Some(address!("0000000000000000000000000000000000000002")),
2222            fee_payer_signature: None,
2223            valid_after: None,
2224            valid_before: None,
2225            access_list: Default::default(),
2226            tempo_authorization_list: authorizations,
2227            key_authorization: None,
2228        };
2229
2230        let signed_tx = AASigned::new_unhashed(
2231            tx_aa,
2232            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
2233        );
2234        let envelope: TempoTxEnvelope = signed_tx.into();
2235        let recovered = envelope.try_into_recovered().unwrap();
2236        TempoPooledTransaction::new(recovered)
2237    }
2238
2239    #[tokio::test]
2240    async fn test_aa_too_many_authorizations_rejected() {
2241        let current_time = std::time::SystemTime::now()
2242            .duration_since(std::time::UNIX_EPOCH)
2243            .unwrap()
2244            .as_secs();
2245
2246        // Create transaction with more authorizations than the default limit
2247        let transaction =
2248            create_aa_transaction_with_authorizations(DEFAULT_MAX_TEMPO_AUTHORIZATIONS + 1);
2249        let validator = setup_validator(&transaction, current_time);
2250
2251        let outcome = validator
2252            .validate_transaction(TransactionOrigin::External, transaction)
2253            .await;
2254
2255        match &outcome {
2256            TransactionValidationOutcome::Invalid(_, err) => {
2257                let error_msg = err.to_string();
2258                assert!(
2259                    error_msg.contains("Too many authorizations"),
2260                    "Expected TooManyAuthorizations error, got: {error_msg}"
2261                );
2262            }
2263            other => panic!("Expected Invalid outcome, got: {other:?}"),
2264        }
2265    }
2266
2267    #[tokio::test]
2268    async fn test_aa_authorization_count_at_limit_accepted() {
2269        let current_time = std::time::SystemTime::now()
2270            .duration_since(std::time::UNIX_EPOCH)
2271            .unwrap()
2272            .as_secs();
2273
2274        // Create transaction with exactly the limit
2275        let transaction =
2276            create_aa_transaction_with_authorizations(DEFAULT_MAX_TEMPO_AUTHORIZATIONS);
2277        let validator = setup_validator(&transaction, current_time);
2278
2279        let outcome = validator
2280            .validate_transaction(TransactionOrigin::External, transaction)
2281            .await;
2282
2283        // Should not fail with TooManyAuthorizations (may fail for other reasons)
2284        if let TransactionValidationOutcome::Invalid(_, err) = &outcome {
2285            let error_msg = err.to_string();
2286            assert!(
2287                !error_msg.contains("Too many authorizations"),
2288                "Should not fail with TooManyAuthorizations at the limit, got: {error_msg}"
2289            );
2290        }
2291    }
2292
2293    /// AA transactions must have at least one call.
2294    #[tokio::test]
2295    async fn test_aa_no_calls_rejected() {
2296        let current_time = std::time::SystemTime::now()
2297            .duration_since(std::time::UNIX_EPOCH)
2298            .unwrap()
2299            .as_secs();
2300
2301        // Create an AA transaction with no calls
2302        let transaction = TxBuilder::aa(Address::random())
2303            .fee_token(address!("0000000000000000000000000000000000000002"))
2304            .calls(vec![]) // Empty calls
2305            .build();
2306        let validator = setup_validator(&transaction, current_time);
2307
2308        let outcome = validator
2309            .validate_transaction(TransactionOrigin::External, transaction)
2310            .await;
2311
2312        match outcome {
2313            TransactionValidationOutcome::Invalid(_, ref err) => {
2314                assert!(
2315                    matches!(
2316                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2317                        Some(TempoPoolTransactionError::Evm(
2318                            TempoInvalidTransaction::CallsValidation(_)
2319                        ))
2320                    ),
2321                    "Expected NoCalls error, got: {err:?}"
2322                );
2323            }
2324            _ => panic!("Expected Invalid outcome with NoCalls error, got: {outcome:?}"),
2325        }
2326    }
2327
2328    /// CREATE calls (contract deployments) must be the first call in an AA transaction.
2329    #[tokio::test]
2330    async fn test_aa_create_call_not_first_rejected() {
2331        let current_time = std::time::SystemTime::now()
2332            .duration_since(std::time::UNIX_EPOCH)
2333            .unwrap()
2334            .as_secs();
2335
2336        // Create an AA transaction with a CREATE call as the second call
2337        let calls = vec![
2338            Call {
2339                to: TxKind::Call(Address::random()), // First call is a regular call
2340                value: U256::ZERO,
2341                input: Default::default(),
2342            },
2343            Call {
2344                to: TxKind::Create, // Second call is a CREATE - should be rejected
2345                value: U256::ZERO,
2346                input: Default::default(),
2347            },
2348        ];
2349
2350        let transaction = TxBuilder::aa(Address::random())
2351            .fee_token(address!("0000000000000000000000000000000000000002"))
2352            .calls(calls)
2353            .build();
2354        let validator = setup_validator(&transaction, current_time);
2355
2356        let outcome = validator
2357            .validate_transaction(TransactionOrigin::External, transaction)
2358            .await;
2359
2360        match outcome {
2361            TransactionValidationOutcome::Invalid(_, ref err) => {
2362                assert!(
2363                    matches!(
2364                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2365                        Some(TempoPoolTransactionError::Evm(
2366                            TempoInvalidTransaction::CallsValidation(_)
2367                        ))
2368                    ),
2369                    "Expected CreateCallNotFirst error, got: {err:?}"
2370                );
2371            }
2372            _ => panic!("Expected Invalid outcome with CreateCallNotFirst error, got: {outcome:?}"),
2373        }
2374    }
2375
2376    /// CREATE call as the first call should be accepted.
2377    #[tokio::test]
2378    async fn test_aa_create_call_first_accepted() {
2379        let current_time = std::time::SystemTime::now()
2380            .duration_since(std::time::UNIX_EPOCH)
2381            .unwrap()
2382            .as_secs();
2383
2384        // Create an AA transaction with a CREATE call as the first call
2385        let calls = vec![
2386            Call {
2387                to: TxKind::Create, // First call is a CREATE - should be accepted
2388                value: U256::ZERO,
2389                input: Default::default(),
2390            },
2391            Call {
2392                to: TxKind::Call(Address::random()), // Second call is a regular call
2393                value: U256::ZERO,
2394                input: Default::default(),
2395            },
2396        ];
2397
2398        let transaction = TxBuilder::aa(Address::random())
2399            .fee_token(address!("0000000000000000000000000000000000000002"))
2400            .calls(calls)
2401            .build();
2402        let validator = setup_validator(&transaction, current_time);
2403
2404        let outcome = validator
2405            .validate_transaction(TransactionOrigin::External, transaction)
2406            .await;
2407
2408        // Should NOT fail with CreateCallNotFirst (may fail for other reasons)
2409        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2410            assert!(
2411                !matches!(
2412                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2413                    Some(TempoPoolTransactionError::Evm(
2414                        TempoInvalidTransaction::CallsValidation(_)
2415                    ))
2416                ),
2417                "CREATE call as first call should be accepted, got: {err:?}"
2418            );
2419        }
2420    }
2421
2422    /// Multiple CREATE calls in the same transaction should be rejected.
2423    #[tokio::test]
2424    async fn test_aa_multiple_creates_rejected() {
2425        let current_time = std::time::SystemTime::now()
2426            .duration_since(std::time::UNIX_EPOCH)
2427            .unwrap()
2428            .as_secs();
2429
2430        // calls = [CREATE, CALL, CREATE] -> should reject with CreateCallNotFirst
2431        let calls = vec![
2432            Call {
2433                to: TxKind::Create, // First call is a CREATE - ok
2434                value: U256::ZERO,
2435                input: Default::default(),
2436            },
2437            Call {
2438                to: TxKind::Call(Address::random()), // Second call is a regular call
2439                value: U256::ZERO,
2440                input: Default::default(),
2441            },
2442            Call {
2443                to: TxKind::Create, // Third call is a CREATE - should be rejected
2444                value: U256::ZERO,
2445                input: Default::default(),
2446            },
2447        ];
2448
2449        let transaction = TxBuilder::aa(Address::random())
2450            .fee_token(address!("0000000000000000000000000000000000000002"))
2451            .calls(calls)
2452            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2453            .build();
2454        let validator = setup_validator(&transaction, current_time);
2455
2456        let outcome = validator
2457            .validate_transaction(TransactionOrigin::External, transaction)
2458            .await;
2459
2460        match outcome {
2461            TransactionValidationOutcome::Invalid(_, ref err) => {
2462                assert!(
2463                    matches!(
2464                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2465                        Some(TempoPoolTransactionError::Evm(
2466                            TempoInvalidTransaction::CallsValidation(_)
2467                        ))
2468                    ),
2469                    "Expected CreateCallNotFirst error, got: {err:?}"
2470                );
2471            }
2472            _ => panic!("Expected Invalid outcome with CreateCallNotFirst error, got: {outcome:?}"),
2473        }
2474    }
2475
2476    /// CREATE calls must not have any entries in the authorization list.
2477    #[tokio::test]
2478    async fn test_aa_create_call_with_authorization_list_rejected() {
2479        use alloy_eips::eip7702::Authorization;
2480        use alloy_primitives::Signature;
2481        use tempo_primitives::transaction::{
2482            TempoSignedAuthorization,
2483            tt_signature::{PrimitiveSignature, TempoSignature},
2484        };
2485
2486        let current_time = std::time::SystemTime::now()
2487            .duration_since(std::time::UNIX_EPOCH)
2488            .unwrap()
2489            .as_secs();
2490
2491        // Create an AA transaction with a CREATE call and a non-empty authorization list
2492        let calls = vec![Call {
2493            to: TxKind::Create, // CREATE call
2494            value: U256::ZERO,
2495            input: Default::default(),
2496        }];
2497
2498        // Create a single authorization entry
2499        let auth = Authorization {
2500            chain_id: U256::from(1),
2501            nonce: 0,
2502            address: address!("0000000000000000000000000000000000000001"),
2503        };
2504        let authorization = TempoSignedAuthorization::new_unchecked(
2505            auth,
2506            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
2507        );
2508
2509        let transaction = TxBuilder::aa(Address::random())
2510            .fee_token(address!("0000000000000000000000000000000000000002"))
2511            .calls(calls)
2512            .authorization_list(vec![authorization])
2513            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2514            .build();
2515        let validator = setup_validator(&transaction, current_time);
2516
2517        let outcome = validator
2518            .validate_transaction(TransactionOrigin::External, transaction)
2519            .await;
2520
2521        match outcome {
2522            TransactionValidationOutcome::Invalid(_, ref err) => {
2523                assert!(
2524                    matches!(
2525                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2526                        Some(TempoPoolTransactionError::Evm(
2527                            TempoInvalidTransaction::CallsValidation(_)
2528                        ))
2529                    ),
2530                    "Expected CreateCallWithAuthorizationList error, got: {err:?}"
2531                );
2532            }
2533            _ => panic!("Expected Invalid outcome, got: {outcome:?}"),
2534        }
2535    }
2536
2537    /// Paused tokens should be rejected as invalid fee tokens.
2538    #[test]
2539    fn test_paused_token_is_invalid_fee_token() {
2540        let fee_token = address!("20C0000000000000000000000000000000000001");
2541
2542        // "USD" = 0x555344, stored in high bytes with length 6 (3*2) in LSB
2543        let usd_currency_value =
2544            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
2545
2546        let provider =
2547            MockEthProvider::default().with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
2548        provider.add_account(
2549            fee_token,
2550            ExtendedAccount::new(0, U256::ZERO).extend_storage([
2551                (tip20_slots::CURRENCY.into(), usd_currency_value),
2552                (tip20_slots::PAUSED.into(), U256::from(1)),
2553            ]),
2554        );
2555
2556        let mut state = provider.latest().unwrap();
2557        let spec = provider.chain_spec().tempo_hardfork_at(0);
2558
2559        // Test that is_fee_token_paused returns true for paused tokens
2560        let result = state.is_fee_token_paused(spec, fee_token);
2561        assert!(result.is_ok());
2562        assert!(
2563            result.unwrap(),
2564            "Paused tokens should be detected as paused"
2565        );
2566    }
2567
2568    /// Non-AA transaction with insufficient gas should be rejected with Invalid outcome
2569    /// and IntrinsicGasTooLow error.
2570    #[tokio::test]
2571    async fn test_non_aa_intrinsic_gas_insufficient_rejected() {
2572        let current_time = std::time::SystemTime::now()
2573            .duration_since(std::time::UNIX_EPOCH)
2574            .unwrap()
2575            .as_secs();
2576
2577        // Create EIP-1559 transaction with very low gas limit (below intrinsic gas of ~21k)
2578        let tx = TxBuilder::eip1559(Address::random())
2579            .gas_limit(1_000) // Way below intrinsic gas
2580            .build_eip1559();
2581
2582        let validator = setup_validator(&tx, current_time);
2583        let outcome = validator
2584            .validate_transaction(TransactionOrigin::External, tx)
2585            .await;
2586
2587        match outcome {
2588            TransactionValidationOutcome::Invalid(_, ref err) => {
2589                assert!(matches!(
2590                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2591                    Some(TempoPoolTransactionError::Evm(
2592                        TempoInvalidTransaction::EthInvalidTransaction(
2593                            InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
2594                        )
2595                    ))
2596                ))
2597            }
2598            TransactionValidationOutcome::Error(_, _) => {
2599                panic!("Expected Invalid outcome, got Error - this was the bug we fixed!")
2600            }
2601            _ => panic!("Expected Invalid outcome with IntrinsicGasTooLow, got: {outcome:?}"),
2602        }
2603    }
2604
2605    /// Non-AA transaction with sufficient gas should pass intrinsic gas validation.
2606    #[tokio::test]
2607    async fn test_non_aa_intrinsic_gas_sufficient_passes() {
2608        let current_time = std::time::SystemTime::now()
2609            .duration_since(std::time::UNIX_EPOCH)
2610            .unwrap()
2611            .as_secs();
2612
2613        // Create EIP-1559 transaction with plenty of gas
2614        let tx = TxBuilder::eip1559(Address::random())
2615            .gas_limit(1_000_000) // Well above intrinsic gas
2616            .build_eip1559();
2617
2618        let validator = setup_validator(&tx, current_time);
2619        let outcome = validator
2620            .validate_transaction(TransactionOrigin::External, tx)
2621            .await;
2622
2623        // Should NOT fail with CallGasCostMoreThanGasLimit (intrinsic gas check)
2624        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2625            assert!(
2626                matches!(err, InvalidPoolTransactionError::IntrinsicGasTooLow),
2627                "Non-AA tx with 100k gas should NOT fail intrinsic gas check, got: {err:?}"
2628            );
2629        }
2630    }
2631
2632    /// Verify intrinsic gas error is returned for insufficient gas.
2633    #[tokio::test]
2634    async fn test_intrinsic_gas_error_contains_gas_details() {
2635        let current_time = std::time::SystemTime::now()
2636            .duration_since(std::time::UNIX_EPOCH)
2637            .unwrap()
2638            .as_secs();
2639
2640        let gas_limit = 5_000u64;
2641        let tx = TxBuilder::eip1559(Address::random())
2642            .gas_limit(gas_limit)
2643            .build_eip1559();
2644
2645        let validator = setup_validator(&tx, current_time);
2646        let outcome = validator
2647            .validate_transaction(TransactionOrigin::External, tx)
2648            .await;
2649
2650        match outcome {
2651            TransactionValidationOutcome::Invalid(_, ref err) => {
2652                assert!(matches!(
2653                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2654                    Some(TempoPoolTransactionError::Evm(
2655                        TempoInvalidTransaction::EthInvalidTransaction(
2656                            InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
2657                        )
2658                    ))
2659                ));
2660            }
2661            _ => panic!("Expected Invalid outcome, got: {outcome:?}"),
2662        }
2663    }
2664
2665    /// Paused validator tokens should be rejected even though they would bypass the liquidity check.
2666    #[test]
2667    fn test_paused_validator_token_rejected_before_liquidity_bypass() {
2668        // Use a TIP20-prefixed address for the fee token
2669        let paused_validator_token = address!("20C0000000000000000000000000000000000001");
2670
2671        // "USD" = 0x555344, stored in high bytes with length 6 (3*2) in LSB
2672        let usd_currency_value =
2673            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
2674
2675        let provider =
2676            MockEthProvider::default().with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
2677
2678        // Set up the token as a valid USD token but PAUSED
2679        provider.add_account(
2680            paused_validator_token,
2681            ExtendedAccount::new(0, U256::ZERO).extend_storage([
2682                (tip20_slots::CURRENCY.into(), usd_currency_value),
2683                (tip20_slots::PAUSED.into(), U256::from(1)),
2684            ]),
2685        );
2686
2687        let mut state = provider.latest().unwrap();
2688        let spec = provider.chain_spec().tempo_hardfork_at(0);
2689
2690        // Create AMM cache with the paused token in unique_tokens (simulating a validator's
2691        // preferred token). This would normally cause has_enough_liquidity() to return true
2692        // immediately at the bypass check.
2693        let amm_cache = AmmLiquidityCache::with_unique_tokens(vec![paused_validator_token]);
2694
2695        // Verify the bypass would apply: the token IS in unique_tokens
2696        assert!(
2697            amm_cache.is_active_validator_token(&paused_validator_token),
2698            "Token should be in unique_tokens for this test"
2699        );
2700
2701        // Verify has_enough_liquidity would bypass (return true) for this token
2702        // because it matches a validator token. This confirms the vulnerability we're testing.
2703        let liquidity_result =
2704            amm_cache.has_enough_liquidity(paused_validator_token, U256::from(1000), &state);
2705        assert!(
2706            liquidity_result.is_ok() && liquidity_result.unwrap(),
2707            "Token in unique_tokens should bypass liquidity check and return true"
2708        );
2709
2710        // BUT the pause check in is_fee_token_paused should catch it BEFORE the bypass
2711        let is_paused = state.is_fee_token_paused(spec, paused_validator_token);
2712        assert!(is_paused.is_ok());
2713        assert!(
2714            is_paused.unwrap(),
2715            "Paused validator token should be detected by is_fee_token_paused BEFORE reaching has_enough_liquidity"
2716        );
2717    }
2718
2719    #[tokio::test]
2720    async fn test_aa_exactly_max_calls_accepted() {
2721        let current_time = std::time::SystemTime::now()
2722            .duration_since(std::time::UNIX_EPOCH)
2723            .unwrap()
2724            .as_secs();
2725
2726        let calls: Vec<Call> = (0..MAX_AA_CALLS)
2727            .map(|_| Call {
2728                to: TxKind::Call(Address::random()),
2729                value: U256::ZERO,
2730                input: Default::default(),
2731            })
2732            .collect();
2733
2734        let transaction = TxBuilder::aa(Address::random())
2735            .fee_token(address!("0000000000000000000000000000000000000002"))
2736            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2737            .calls(calls)
2738            .build();
2739        let validator = setup_validator(&transaction, current_time);
2740
2741        let outcome = validator
2742            .validate_transaction(TransactionOrigin::External, transaction)
2743            .await;
2744
2745        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2746            assert!(
2747                !matches!(
2748                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2749                    Some(TempoPoolTransactionError::TooManyCalls { .. })
2750                ),
2751                "Exactly MAX_AA_CALLS calls should not trigger TooManyCalls, got: {err:?}"
2752            );
2753        }
2754    }
2755
2756    #[tokio::test]
2757    async fn test_aa_too_many_calls_rejected() {
2758        let current_time = std::time::SystemTime::now()
2759            .duration_since(std::time::UNIX_EPOCH)
2760            .unwrap()
2761            .as_secs();
2762
2763        let calls: Vec<Call> = (0..MAX_AA_CALLS + 1)
2764            .map(|_| Call {
2765                to: TxKind::Call(Address::random()),
2766                value: U256::ZERO,
2767                input: Default::default(),
2768            })
2769            .collect();
2770
2771        let transaction = TxBuilder::aa(Address::random())
2772            .fee_token(address!("0000000000000000000000000000000000000002"))
2773            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2774            .calls(calls)
2775            .build();
2776        let validator = setup_validator(&transaction, current_time);
2777
2778        let outcome = validator
2779            .validate_transaction(TransactionOrigin::External, transaction)
2780            .await;
2781
2782        match outcome {
2783            TransactionValidationOutcome::Invalid(_, ref err) => {
2784                assert!(
2785                    matches!(
2786                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2787                        Some(TempoPoolTransactionError::TooManyCalls { .. })
2788                    ),
2789                    "Expected TooManyCalls error, got: {err:?}"
2790                );
2791            }
2792            _ => panic!("Expected Invalid outcome with TooManyCalls error, got: {outcome:?}"),
2793        }
2794    }
2795
2796    #[tokio::test]
2797    async fn test_aa_exactly_max_call_input_size_accepted() {
2798        let current_time = std::time::SystemTime::now()
2799            .duration_since(std::time::UNIX_EPOCH)
2800            .unwrap()
2801            .as_secs();
2802
2803        let calls = vec![Call {
2804            to: TxKind::Call(Address::random()),
2805            value: U256::ZERO,
2806            input: vec![0u8; MAX_CALL_INPUT_SIZE].into(),
2807        }];
2808
2809        let transaction = TxBuilder::aa(Address::random())
2810            .fee_token(address!("0000000000000000000000000000000000000002"))
2811            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2812            .calls(calls)
2813            .build();
2814        let validator = setup_validator(&transaction, current_time);
2815
2816        let outcome = validator
2817            .validate_transaction(TransactionOrigin::External, transaction)
2818            .await;
2819
2820        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2821            assert!(
2822                !matches!(
2823                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2824                    Some(TempoPoolTransactionError::CallInputTooLarge { .. })
2825                ),
2826                "Exactly MAX_CALL_INPUT_SIZE input should not trigger CallInputTooLarge, got: {err:?}"
2827            );
2828        }
2829    }
2830
2831    #[tokio::test]
2832    async fn test_aa_call_input_too_large_rejected() {
2833        let current_time = std::time::SystemTime::now()
2834            .duration_since(std::time::UNIX_EPOCH)
2835            .unwrap()
2836            .as_secs();
2837
2838        let calls = vec![Call {
2839            to: TxKind::Call(Address::random()),
2840            value: U256::ZERO,
2841            input: vec![0u8; MAX_CALL_INPUT_SIZE + 1].into(),
2842        }];
2843
2844        let transaction = TxBuilder::aa(Address::random())
2845            .fee_token(address!("0000000000000000000000000000000000000002"))
2846            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2847            .calls(calls)
2848            .build();
2849        let validator = setup_validator(&transaction, current_time);
2850
2851        let outcome = validator
2852            .validate_transaction(TransactionOrigin::External, transaction)
2853            .await;
2854
2855        match outcome {
2856            TransactionValidationOutcome::Invalid(_, ref err) => {
2857                let is_oversized = matches!(err, InvalidPoolTransactionError::OversizedData { .. });
2858                let is_call_input_too_large = matches!(
2859                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2860                    Some(TempoPoolTransactionError::CallInputTooLarge { .. })
2861                );
2862                assert!(
2863                    is_oversized || is_call_input_too_large,
2864                    "Expected OversizedData or CallInputTooLarge error, got: {err:?}"
2865                );
2866            }
2867            _ => panic!("Expected Invalid outcome, got: {outcome:?}"),
2868        }
2869    }
2870
2871    #[tokio::test]
2872    async fn test_aa_exactly_max_access_list_accounts_accepted() {
2873        use alloy_eips::eip2930::{AccessList, AccessListItem};
2874
2875        let current_time = std::time::SystemTime::now()
2876            .duration_since(std::time::UNIX_EPOCH)
2877            .unwrap()
2878            .as_secs();
2879
2880        let items: Vec<AccessListItem> = (0..MAX_ACCESS_LIST_ACCOUNTS)
2881            .map(|_| AccessListItem {
2882                address: Address::random(),
2883                storage_keys: vec![],
2884            })
2885            .collect();
2886
2887        let transaction = TxBuilder::aa(Address::random())
2888            .fee_token(address!("0000000000000000000000000000000000000002"))
2889            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2890            .access_list(AccessList(items))
2891            .build();
2892        let validator = setup_validator(&transaction, current_time);
2893
2894        let outcome = validator
2895            .validate_transaction(TransactionOrigin::External, transaction)
2896            .await;
2897
2898        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2899            assert!(
2900                !matches!(
2901                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2902                    Some(TempoPoolTransactionError::TooManyAccessListAccounts { .. })
2903                ),
2904                "Exactly MAX_ACCESS_LIST_ACCOUNTS should not trigger TooManyAccessListAccounts, got: {err:?}"
2905            );
2906        }
2907    }
2908
2909    #[tokio::test]
2910    async fn test_aa_too_many_access_list_accounts_rejected() {
2911        use alloy_eips::eip2930::{AccessList, AccessListItem};
2912
2913        let current_time = std::time::SystemTime::now()
2914            .duration_since(std::time::UNIX_EPOCH)
2915            .unwrap()
2916            .as_secs();
2917
2918        let items: Vec<AccessListItem> = (0..MAX_ACCESS_LIST_ACCOUNTS + 1)
2919            .map(|_| AccessListItem {
2920                address: Address::random(),
2921                storage_keys: vec![],
2922            })
2923            .collect();
2924
2925        let transaction = TxBuilder::aa(Address::random())
2926            .fee_token(address!("0000000000000000000000000000000000000002"))
2927            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2928            .access_list(AccessList(items))
2929            .build();
2930        let validator = setup_validator(&transaction, current_time);
2931
2932        let outcome = validator
2933            .validate_transaction(TransactionOrigin::External, transaction)
2934            .await;
2935
2936        match outcome {
2937            TransactionValidationOutcome::Invalid(_, ref err) => {
2938                assert!(
2939                    matches!(
2940                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2941                        Some(TempoPoolTransactionError::TooManyAccessListAccounts { .. })
2942                    ),
2943                    "Expected TooManyAccessListAccounts error, got: {err:?}"
2944                );
2945            }
2946            _ => panic!(
2947                "Expected Invalid outcome with TooManyAccessListAccounts error, got: {outcome:?}"
2948            ),
2949        }
2950    }
2951
2952    #[tokio::test]
2953    async fn test_aa_exactly_max_storage_keys_per_account_accepted() {
2954        use alloy_eips::eip2930::{AccessList, AccessListItem};
2955
2956        let current_time = std::time::SystemTime::now()
2957            .duration_since(std::time::UNIX_EPOCH)
2958            .unwrap()
2959            .as_secs();
2960
2961        let items = vec![AccessListItem {
2962            address: Address::random(),
2963            storage_keys: (0..MAX_STORAGE_KEYS_PER_ACCOUNT)
2964                .map(|_| B256::random())
2965                .collect(),
2966        }];
2967
2968        let transaction = TxBuilder::aa(Address::random())
2969            .fee_token(address!("0000000000000000000000000000000000000002"))
2970            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
2971            .access_list(AccessList(items))
2972            .build();
2973        let validator = setup_validator(&transaction, current_time);
2974
2975        let outcome = validator
2976            .validate_transaction(TransactionOrigin::External, transaction)
2977            .await;
2978
2979        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2980            assert!(
2981                !matches!(
2982                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2983                    Some(TempoPoolTransactionError::TooManyStorageKeysPerAccount { .. })
2984                ),
2985                "Exactly MAX_STORAGE_KEYS_PER_ACCOUNT should not trigger TooManyStorageKeysPerAccount, got: {err:?}"
2986            );
2987        }
2988    }
2989
2990    #[tokio::test]
2991    async fn test_aa_too_many_storage_keys_per_account_rejected() {
2992        use alloy_eips::eip2930::{AccessList, AccessListItem};
2993
2994        let current_time = std::time::SystemTime::now()
2995            .duration_since(std::time::UNIX_EPOCH)
2996            .unwrap()
2997            .as_secs();
2998
2999        let items = vec![AccessListItem {
3000            address: Address::random(),
3001            storage_keys: (0..MAX_STORAGE_KEYS_PER_ACCOUNT + 1)
3002                .map(|_| B256::random())
3003                .collect(),
3004        }];
3005
3006        let transaction = TxBuilder::aa(Address::random())
3007            .fee_token(address!("0000000000000000000000000000000000000002"))
3008            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
3009            .access_list(AccessList(items))
3010            .build();
3011        let validator = setup_validator(&transaction, current_time);
3012
3013        let outcome = validator
3014            .validate_transaction(TransactionOrigin::External, transaction)
3015            .await;
3016
3017        match outcome {
3018            TransactionValidationOutcome::Invalid(_, ref err) => {
3019                assert!(
3020                    matches!(
3021                        err.downcast_other_ref::<TempoPoolTransactionError>(),
3022                        Some(TempoPoolTransactionError::TooManyStorageKeysPerAccount { .. })
3023                    ),
3024                    "Expected TooManyStorageKeysPerAccount error, got: {err:?}"
3025                );
3026            }
3027            _ => panic!(
3028                "Expected Invalid outcome with TooManyStorageKeysPerAccount error, got: {outcome:?}"
3029            ),
3030        }
3031    }
3032
3033    #[tokio::test]
3034    async fn test_aa_exactly_max_total_storage_keys_accepted() {
3035        use alloy_eips::eip2930::{AccessList, AccessListItem};
3036
3037        let current_time = std::time::SystemTime::now()
3038            .duration_since(std::time::UNIX_EPOCH)
3039            .unwrap()
3040            .as_secs();
3041
3042        let keys_per_account = MAX_STORAGE_KEYS_PER_ACCOUNT;
3043        let num_accounts = MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL / keys_per_account;
3044        let items: Vec<AccessListItem> = (0..num_accounts)
3045            .map(|_| AccessListItem {
3046                address: Address::random(),
3047                storage_keys: (0..keys_per_account).map(|_| B256::random()).collect(),
3048            })
3049            .collect();
3050        assert_eq!(
3051            items.iter().map(|i| i.storage_keys.len()).sum::<usize>(),
3052            MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL
3053        );
3054
3055        let transaction = TxBuilder::aa(Address::random())
3056            .fee_token(address!("0000000000000000000000000000000000000002"))
3057            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
3058            .access_list(AccessList(items))
3059            .build();
3060        let validator = setup_validator(&transaction, current_time);
3061
3062        let outcome = validator
3063            .validate_transaction(TransactionOrigin::External, transaction)
3064            .await;
3065
3066        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
3067            assert!(
3068                !matches!(
3069                    err.downcast_other_ref::<TempoPoolTransactionError>(),
3070                    Some(TempoPoolTransactionError::TooManyTotalStorageKeys { .. })
3071                ),
3072                "Exactly MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL should not trigger TooManyTotalStorageKeys, got: {err:?}"
3073            );
3074        }
3075    }
3076
3077    #[tokio::test]
3078    async fn test_aa_too_many_total_storage_keys_rejected() {
3079        use alloy_eips::eip2930::{AccessList, AccessListItem};
3080
3081        let current_time = std::time::SystemTime::now()
3082            .duration_since(std::time::UNIX_EPOCH)
3083            .unwrap()
3084            .as_secs();
3085
3086        let keys_per_account = MAX_STORAGE_KEYS_PER_ACCOUNT;
3087        let num_accounts = MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL / keys_per_account;
3088        let mut items: Vec<AccessListItem> = (0..num_accounts)
3089            .map(|_| AccessListItem {
3090                address: Address::random(),
3091                storage_keys: (0..keys_per_account).map(|_| B256::random()).collect(),
3092            })
3093            .collect();
3094        items.push(AccessListItem {
3095            address: Address::random(),
3096            storage_keys: vec![B256::random()],
3097        });
3098        assert_eq!(
3099            items.iter().map(|i| i.storage_keys.len()).sum::<usize>(),
3100            MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL + 1
3101        );
3102
3103        let transaction = TxBuilder::aa(Address::random())
3104            .fee_token(address!("0000000000000000000000000000000000000002"))
3105            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
3106            .access_list(AccessList(items))
3107            .build();
3108        let validator = setup_validator(&transaction, current_time);
3109
3110        let outcome = validator
3111            .validate_transaction(TransactionOrigin::External, transaction)
3112            .await;
3113
3114        match outcome {
3115            TransactionValidationOutcome::Invalid(_, ref err) => {
3116                assert!(
3117                    matches!(
3118                        err.downcast_other_ref::<TempoPoolTransactionError>(),
3119                        Some(TempoPoolTransactionError::TooManyTotalStorageKeys { .. })
3120                    ),
3121                    "Expected TooManyTotalStorageKeys error, got: {err:?}"
3122                );
3123            }
3124            _ => panic!(
3125                "Expected Invalid outcome with TooManyTotalStorageKeys error, got: {outcome:?}"
3126            ),
3127        }
3128    }
3129}