Skip to main content

tempo_transaction_pool/
validator.rs

1use crate::{
2    amm::AmmLiquidityCache,
3    transaction::{TempoPoolTransactionError, TempoPooledTransaction},
4};
5use alloy_consensus::Transaction;
6
7use alloy_primitives::{Address, U256};
8use reth_chainspec::{ChainSpecProvider, EthChainSpec};
9use reth_primitives_traits::{
10    GotExpected, SealedBlock, transaction::error::InvalidTransactionError,
11};
12use reth_storage_api::{StateProvider, StateProviderFactory, errors::ProviderError};
13use reth_transaction_pool::{
14    EthTransactionValidator, PoolTransaction, TransactionOrigin, TransactionValidationOutcome,
15    TransactionValidator, error::InvalidPoolTransactionError,
16};
17use revm::context_interface::cfg::GasId;
18use tempo_chainspec::{
19    TempoChainSpec,
20    hardfork::{TempoHardfork, TempoHardforks},
21};
22use tempo_evm::TempoEvmConfig;
23#[cfg(test)]
24use tempo_precompiles::{ACCOUNT_KEYCHAIN_ADDRESS, account_keychain::AuthorizedKey};
25use tempo_precompiles::{
26    account_keychain::AccountKeychain,
27    nonce::{INonce, NonceManager},
28    storage::Handler,
29};
30use tempo_primitives::{
31    Block,
32    subblock::has_sub_block_nonce_key_prefix,
33    transaction::{
34        RecoveredTempoAuthorization, TEMPO_EXPIRING_NONCE_KEY,
35        TEMPO_EXPIRING_NONCE_MAX_EXPIRY_SECS, TempoTransaction,
36    },
37};
38use tempo_revm::{
39    TempoBatchCallEnv, TempoStateAccess, calculate_aa_batch_intrinsic_gas,
40    gas_params::{TempoGasParams, tempo_gas_params},
41    handler::EXPIRING_NONCE_GAS,
42};
43
44// Reject AA txs where `valid_before` is too close to current time (or already expired) to prevent block invalidation.
45const AA_VALID_BEFORE_MIN_SECS: u64 = 3;
46
47/// Default maximum number of authorizations allowed in an AA transaction's authorization list.
48pub const DEFAULT_MAX_TEMPO_AUTHORIZATIONS: usize = 16;
49
50/// Maximum number of calls allowed per AA transaction (DoS protection).
51pub const MAX_AA_CALLS: usize = 32;
52
53/// Maximum size of input data per call in bytes (128KB, DoS protection).
54pub const MAX_CALL_INPUT_SIZE: usize = 128 * 1024;
55
56/// Maximum number of accounts in the access list (DoS protection).
57pub const MAX_ACCESS_LIST_ACCOUNTS: usize = 256;
58
59/// Maximum number of storage keys per account in the access list (DoS protection).
60pub const MAX_STORAGE_KEYS_PER_ACCOUNT: usize = 256;
61
62/// Maximum total number of storage keys across all accounts in the access list (DoS protection).
63pub const MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL: usize = 2048;
64
65/// Maximum number of token limits in a KeyAuthorization (DoS protection).
66pub const MAX_TOKEN_LIMITS: usize = 256;
67
68/// Default maximum allowed `valid_after` offset for AA txs (in seconds).
69///
70/// Aligned with the default queued transaction lifetime (`max_queued_lifetime = 120s`)
71/// so that transactions with a future `valid_after` are not silently evicted before
72/// they become executable.
73pub const DEFAULT_AA_VALID_AFTER_MAX_SECS: u64 = 120;
74
75/// Validator for Tempo transactions.
76#[derive(Debug)]
77pub struct TempoTransactionValidator<Client> {
78    /// Inner validator that performs default Ethereum tx validation.
79    pub(crate) inner: EthTransactionValidator<Client, TempoPooledTransaction, TempoEvmConfig>,
80    /// Maximum allowed `valid_after` offset for AA txs.
81    pub(crate) aa_valid_after_max_secs: u64,
82    /// Maximum number of authorizations allowed in an AA transaction.
83    pub(crate) max_tempo_authorizations: usize,
84    /// Cache of AMM liquidity for validator tokens.
85    pub(crate) amm_liquidity_cache: AmmLiquidityCache,
86}
87
88impl<Client> TempoTransactionValidator<Client>
89where
90    Client: ChainSpecProvider<ChainSpec = TempoChainSpec> + StateProviderFactory,
91{
92    pub fn new(
93        inner: EthTransactionValidator<Client, TempoPooledTransaction, TempoEvmConfig>,
94        aa_valid_after_max_secs: u64,
95        max_tempo_authorizations: usize,
96        amm_liquidity_cache: AmmLiquidityCache,
97    ) -> Self {
98        Self {
99            inner,
100            aa_valid_after_max_secs,
101            max_tempo_authorizations,
102            amm_liquidity_cache,
103        }
104    }
105
106    /// Obtains a clone of the shared [`AmmLiquidityCache`].
107    pub fn amm_liquidity_cache(&self) -> AmmLiquidityCache {
108        self.amm_liquidity_cache.clone()
109    }
110
111    /// Returns the configured client
112    pub fn client(&self) -> &Client {
113        self.inner.client()
114    }
115
116    /// Validates that keychain transactions specify the expected version
117    /// depending on the current chainspec.
118    fn validate_keychain_version(
119        &self,
120        transaction: &TempoPooledTransaction,
121        spec: TempoHardfork,
122    ) -> Result<(), TempoPoolTransactionError> {
123        let Some(tx) = transaction.inner().as_aa() else {
124            return Ok(());
125        };
126
127        if let Err(e) = tx.signature().validate_version(spec.is_t1c()) {
128            return Err(e.into());
129        }
130        for auth_sig in &tx.tx().tempo_authorization_list {
131            if let Err(e) = auth_sig.signature().validate_version(spec.is_t1c()) {
132                return Err(e.into());
133            }
134        }
135
136        Ok(())
137    }
138
139    fn validate_spending_limit(
140        &self,
141        transaction: &TempoPooledTransaction,
142        fee_token: Address,
143        remaining_limit: U256,
144    ) -> Result<(), TempoPoolTransactionError> {
145        let fee_cost = transaction.fee_token_cost();
146        if fee_cost > remaining_limit {
147            return Err(TempoPoolTransactionError::SpendingLimitExceeded {
148                fee_token,
149                cost: fee_cost,
150                remaining: remaining_limit,
151            });
152        }
153
154        Ok(())
155    }
156
157    /// Validates AA transactions against the keychain: signature recovery, key authorization,
158    /// on-chain key existence/revocation/expiry, and spending limits.
159    ///
160    /// Version checks are handled separately by [`Self::validate_keychain_version`] early
161    /// in the path to ensure permanently invalid signatures trigger proper peer penalties.
162    fn validate_against_keychain(
163        &self,
164        transaction: &TempoPooledTransaction,
165        state_provider: &mut impl StateProvider,
166        fee_payer: Address,
167        fee_token: Address,
168    ) -> Result<Result<(), TempoPoolTransactionError>, ProviderError> {
169        let Some(tx) = transaction.inner().as_aa() else {
170            return Ok(Ok(()));
171        };
172
173        let current_time = self.inner.fork_tracker().tip_timestamp();
174        let spec = self.inner.chain_spec().tempo_hardfork_at(current_time);
175
176        let auth = tx.tx().key_authorization.as_ref();
177
178        // Ensure that key auth is valid if present.
179        if let Some(auth) = auth {
180            // Validate signature
181            if !auth
182                .recover_signer()
183                .is_ok_and(|signer| signer == transaction.sender())
184            {
185                return Ok(Err(TempoPoolTransactionError::Keychain(
186                    "Invalid KeyAuthorization signature",
187                )));
188            }
189
190            // Validate chain_id.
191            // T1C+: chain_id must exactly match (wildcard 0 is no longer allowed).
192            // Pre-T1C: chain_id == 0 is wildcard, works on any chain.
193            if auth
194                .validate_chain_id(self.inner.chain_spec().chain_id(), spec.is_t1c())
195                .is_err()
196            {
197                return Ok(Err(TempoPoolTransactionError::Keychain(
198                    "KeyAuthorization chain_id does not match current chain",
199                )));
200            }
201
202            // Validate KeyAuthorization expiry, reject if expiring within the propagation
203            // buffer. This prevents near-expiry authorizations from entering the pool only to
204            // expire at peers with slightly newer tip timestamps.
205            let min_allowed = current_time.saturating_add(AA_VALID_BEFORE_MIN_SECS);
206            if let Some(expiry) = auth.expiry
207                && expiry <= min_allowed
208            {
209                return Ok(Err(TempoPoolTransactionError::KeyAuthorizationExpired {
210                    expiry,
211                    min_allowed,
212                }));
213            }
214        }
215
216        let Some(sig) = tx.signature().as_keychain() else {
217            return Ok(Ok(()));
218        };
219
220        // This should never fail because we set sender based on the sig.
221        if sig.user_address != transaction.sender() {
222            return Ok(Err(TempoPoolTransactionError::Keychain(
223                "Keychain signature user_address does not match sender",
224            )));
225        }
226
227        // This should never happen because we validate the signature validity in `recover_signer`.
228        let Ok(key_id) = sig.key_id(&tx.signature_hash()) else {
229            return Ok(Err(TempoPoolTransactionError::Keychain(
230                "Failed to recover access key ID from Keychain signature",
231            )));
232        };
233
234        let authorized_key = state_provider
235            .with_read_only_storage_ctx(spec, || {
236                AccountKeychain::new().keys[transaction.sender()][key_id].read()
237            })
238            .map_err(ProviderError::other)?;
239
240        // Inline key authorization must still be validated against current key state and
241        // fee-token spending limits to prevent deterministic execution failures from entering
242        // the pool.
243        if let Some(auth) = auth {
244            if auth.key_id != key_id {
245                return Ok(Err(TempoPoolTransactionError::Keychain(
246                    "KeyAuthorization key_id does not match Keychain signature key_id",
247                )));
248            }
249
250            if authorized_key.expiry > 0 {
251                return Ok(Err(TempoPoolTransactionError::Keychain(
252                    "access key already exists",
253                )));
254            }
255
256            if authorized_key.is_revoked {
257                return Ok(Err(TempoPoolTransactionError::Keychain(
258                    "access key has been revoked",
259                )));
260            }
261
262            if let Some(expiry) = auth.expiry
263                && expiry < u64::MAX
264            {
265                transaction.set_key_expiry(Some(expiry));
266            }
267
268            if fee_payer == transaction.sender()
269                && let Some(limits) = &auth.limits
270            {
271                let remaining_limit = limits
272                    .iter()
273                    .rev()
274                    .find(|limit| limit.token == fee_token)
275                    .map(|limit| limit.limit)
276                    .unwrap_or(U256::ZERO);
277
278                if let Err(err) =
279                    self.validate_spending_limit(transaction, fee_token, remaining_limit)
280                {
281                    return Ok(Err(err));
282                }
283            }
284
285            return Ok(Ok(()));
286        }
287
288        // Check if key was revoked (revoked keys cannot be used)
289        if authorized_key.is_revoked {
290            return Ok(Err(TempoPoolTransactionError::Keychain(
291                "access key has been revoked",
292            )));
293        }
294
295        // Check if key exists (key exists if expiry > 0)
296        if authorized_key.expiry == 0 {
297            return Ok(Err(TempoPoolTransactionError::Keychain(
298                "access key does not exist",
299            )));
300        }
301
302        // Check if key has expired or is expiring within the propagation buffer, reject
303        // transactions using near-expiry access keys to prevent them from entering the pool
304        // only to expire at peers with slightly newer tip timestamps.
305        let min_allowed = current_time.saturating_add(AA_VALID_BEFORE_MIN_SECS);
306        if authorized_key.expiry <= min_allowed {
307            return Ok(Err(TempoPoolTransactionError::AccessKeyExpired {
308                expiry: authorized_key.expiry,
309                min_allowed,
310            }));
311        }
312
313        // Cache key expiry for pool maintenance eviction (only if finite expiry)
314        if authorized_key.expiry < u64::MAX {
315            transaction.set_key_expiry(Some(authorized_key.expiry));
316        }
317
318        // Check spending limit for fee token if enforce_limits is enabled.
319        // This prevents transactions that would exceed the spending limit from entering the pool.
320        if fee_payer == transaction.sender() && authorized_key.enforce_limits {
321            // Compute the storage slot for the spending limit
322            let limit_key = AccountKeychain::spending_limit_key(transaction.sender(), key_id);
323            let remaining_limit = state_provider
324                .with_read_only_storage_ctx(spec, || {
325                    AccountKeychain::new().spending_limits[limit_key][fee_token].read()
326                })
327                .map_err(ProviderError::other)?;
328
329            if let Err(err) = self.validate_spending_limit(transaction, fee_token, remaining_limit)
330            {
331                return Ok(Err(err));
332            }
333        }
334
335        Ok(Ok(()))
336    }
337
338    /// Validates that an AA transaction does not exceed the maximum authorization list size.
339    fn ensure_authorization_list_size(
340        &self,
341        transaction: &TempoPooledTransaction,
342    ) -> Result<(), TempoPoolTransactionError> {
343        let Some(aa_tx) = transaction.inner().as_aa() else {
344            return Ok(());
345        };
346
347        let count = aa_tx.tx().tempo_authorization_list.len();
348        if count > self.max_tempo_authorizations {
349            return Err(TempoPoolTransactionError::TooManyAuthorizations {
350                count,
351                max_allowed: self.max_tempo_authorizations,
352            });
353        }
354
355        Ok(())
356    }
357
358    /// Validates AA transaction time-bound conditionals
359    fn ensure_valid_conditionals(
360        &self,
361        tx: &TempoTransaction,
362    ) -> Result<(), TempoPoolTransactionError> {
363        let current_time = self.inner.fork_tracker().tip_timestamp();
364
365        // Check if T1 is active for expiring nonce specific validations
366        let spec = self.inner.chain_spec().tempo_hardfork_at(current_time);
367        let is_expiring_nonce = tx.is_expiring_nonce_tx() && spec.is_t1();
368
369        // Expiring nonce transactions MUST have valid_before set
370        if is_expiring_nonce && tx.valid_before.is_none() {
371            return Err(TempoPoolTransactionError::ExpiringNonceMissingValidBefore);
372        }
373
374        // Expiring nonce transactions MUST have nonce == 0
375        if is_expiring_nonce && tx.nonce != 0 {
376            return Err(TempoPoolTransactionError::ExpiringNonceNonceNotZero);
377        }
378
379        // Reject AA txs where `valid_before` is too close to current time (or already expired).
380        if let Some(valid_before) = tx.valid_before {
381            // Uses tip_timestamp, as if the node is lagging lagging, the maintenance task will evict expired txs.
382            let min_allowed = current_time.saturating_add(AA_VALID_BEFORE_MIN_SECS);
383            if valid_before <= min_allowed {
384                return Err(TempoPoolTransactionError::InvalidValidBefore {
385                    valid_before,
386                    min_allowed,
387                });
388            }
389
390            // For expiring nonce transactions, valid_before must also be within the max expiry window
391            if is_expiring_nonce {
392                let max_allowed = current_time.saturating_add(TEMPO_EXPIRING_NONCE_MAX_EXPIRY_SECS);
393                if valid_before > max_allowed {
394                    return Err(TempoPoolTransactionError::ExpiringNonceValidBeforeTooFar {
395                        valid_before,
396                        max_allowed,
397                    });
398                }
399            }
400        }
401
402        // Reject AA txs where `valid_after` is too far in the future.
403        if let Some(valid_after) = tx.valid_after {
404            // Uses local time to avoid rejecting valid txs when node is lagging.
405            let current_time = std::time::SystemTime::now()
406                .duration_since(std::time::UNIX_EPOCH)
407                .map(|d| d.as_secs())
408                .unwrap_or(0);
409            let max_allowed = current_time.saturating_add(self.aa_valid_after_max_secs);
410            if valid_after > max_allowed {
411                return Err(TempoPoolTransactionError::InvalidValidAfter {
412                    valid_after,
413                    max_allowed,
414                });
415            }
416        }
417
418        Ok(())
419    }
420
421    /// Validates that the gas limit of an AA transaction is sufficient for its intrinsic gas cost.
422    ///
423    /// This prevents transactions from being admitted to the mempool that would fail during execution
424    /// due to insufficient gas for:
425    /// - Per-call cold account access (2600 gas per call target)
426    /// - Calldata gas for ALL calls in the batch
427    /// - Signature verification gas (P256/WebAuthn signatures)
428    /// - Per-call CREATE costs
429    /// - Key authorization costs
430    /// - 2D nonce gas (if nonce_key != 0)
431    ///
432    /// Without this validation, malicious transactions could clog the mempool at zero cost by
433    /// passing pool validation (which only sees the first call's input) but failing at execution time.
434    fn ensure_aa_intrinsic_gas(
435        &self,
436        transaction: &TempoPooledTransaction,
437        spec: TempoHardfork,
438        state_provider: &impl StateProvider,
439    ) -> Result<(), TempoPoolTransactionError> {
440        let sender = transaction.sender();
441        let Some(aa_tx) = transaction.inner().as_aa() else {
442            return Ok(());
443        };
444
445        let tx = aa_tx.tx();
446
447        // Build the TempoBatchCallEnv needed for gas calculation
448        let aa_env = TempoBatchCallEnv {
449            signature: aa_tx.signature().clone(),
450            valid_before: tx.valid_before,
451            valid_after: tx.valid_after,
452            aa_calls: tx.calls.clone(),
453            tempo_authorization_list: tx
454                .tempo_authorization_list
455                .iter()
456                .map(|auth| RecoveredTempoAuthorization::recover(auth.clone()))
457                .collect(),
458            nonce_key: tx.nonce_key,
459            subblock_transaction: tx.subblock_proposer().is_some(),
460            key_authorization: tx.key_authorization.clone(),
461            signature_hash: aa_tx.signature_hash(),
462            tx_hash: *aa_tx.hash(),
463            expiring_nonce_hash: tx
464                .is_expiring_nonce_tx()
465                .then(|| aa_tx.expiring_nonce_hash(sender)),
466            override_key_id: None,
467        };
468
469        // Calculate the intrinsic gas for the AA transaction
470        let gas_params = tempo_gas_params(spec);
471
472        let mut init_and_floor_gas = calculate_aa_batch_intrinsic_gas(
473            &aa_env,
474            &gas_params,
475            Some(tx.access_list.iter()),
476            spec,
477        )
478        .map_err(|_| TempoPoolTransactionError::NonZeroValue)?;
479
480        // Add nonce gas based on hardfork
481        // If tx nonce is 0, it's a new key (0 -> 1 transition), otherwise existing key
482        if spec.is_t1() {
483            // Expiring nonce transactions
484            if tx.nonce_key == TEMPO_EXPIRING_NONCE_KEY {
485                init_and_floor_gas.initial_gas += EXPIRING_NONCE_GAS;
486            } else if tx.nonce == 0 {
487                // TIP-1000: Storage pricing updates for launch
488                // Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas
489                init_and_floor_gas.initial_gas += gas_params.get(GasId::new_account_cost());
490            } else if !tx.nonce_key.is_zero() {
491                // Existing 2D nonce key (nonce > 0): cold SLOAD + warm SSTORE reset
492                // TIP-1000 Invariant 3: existing state updates charge 5,000 gas
493                init_and_floor_gas.initial_gas += spec.gas_existing_nonce_key();
494            }
495            // In CREATE tx with 2d nonce, check if account.nonce is 0, if so, add 250,000 gas.
496            // This covers caller creation of account.
497            if !tx.nonce_key.is_zero()
498                && tx.is_create()
499                // in case of provider error, we assume the account nonce is 0 and charge additional gas.
500                && state_provider
501                    .account_nonce(&sender)
502                    .ok()
503                    .flatten()
504                    .unwrap_or_default()
505                    == 0
506            {
507                init_and_floor_gas.initial_gas += gas_params.get(GasId::new_account_cost());
508            }
509        } else if !tx.nonce_key.is_zero() {
510            // Pre-T1: Add 2D nonce gas if nonce_key is non-zero
511            if tx.nonce == 0 {
512                // New key - cold SLOAD + SSTORE set (0 -> non-zero)
513                init_and_floor_gas.initial_gas += spec.gas_new_nonce_key();
514            } else {
515                // Existing key - cold SLOAD + warm SSTORE reset
516                init_and_floor_gas.initial_gas += spec.gas_existing_nonce_key();
517            }
518        }
519
520        let gas_limit = tx.gas_limit;
521
522        // Check if gas limit is sufficient for initial gas
523        if gas_limit < init_and_floor_gas.initial_gas {
524            return Err(
525                TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost {
526                    gas_limit,
527                    intrinsic_gas: init_and_floor_gas.initial_gas,
528                },
529            );
530        }
531
532        // Check floor gas (Prague+ / EIP-7623)
533        if gas_limit < init_and_floor_gas.floor_gas {
534            return Err(
535                TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost {
536                    gas_limit,
537                    intrinsic_gas: init_and_floor_gas.floor_gas,
538                },
539            );
540        }
541
542        Ok(())
543    }
544
545    /// Validates AA transaction field limits (calls, access list, token limits).
546    ///
547    /// These limits are enforced at the pool level rather than RLP decoding to:
548    /// - Keep the core transaction format flexible
549    /// - Allow peer penalization for sending bad transactions
550    fn ensure_aa_field_limits(
551        &self,
552        transaction: &TempoPooledTransaction,
553    ) -> Result<(), TempoPoolTransactionError> {
554        let Some(aa_tx) = transaction.inner().as_aa() else {
555            return Ok(());
556        };
557
558        let tx = aa_tx.tx();
559
560        if tx.calls.is_empty() {
561            return Err(TempoPoolTransactionError::NoCalls);
562        }
563
564        // Check number of calls
565        if tx.calls.len() > MAX_AA_CALLS {
566            return Err(TempoPoolTransactionError::TooManyCalls {
567                count: tx.calls.len(),
568                max_allowed: MAX_AA_CALLS,
569            });
570        }
571
572        // Check each call's input size
573        for (idx, call) in tx.calls.iter().enumerate() {
574            if call.to.is_create() {
575                // CREATE call must be the first call in the transaction.
576                if idx != 0 {
577                    return Err(TempoPoolTransactionError::CreateCallNotFirst);
578                }
579                // CREATE calls are not allowed in transactions with an authorization list.
580                if !tx.tempo_authorization_list.is_empty() {
581                    return Err(TempoPoolTransactionError::CreateCallWithAuthorizationList);
582                }
583            }
584
585            if call.input.len() > MAX_CALL_INPUT_SIZE {
586                return Err(TempoPoolTransactionError::CallInputTooLarge {
587                    call_index: idx,
588                    size: call.input.len(),
589                    max_allowed: MAX_CALL_INPUT_SIZE,
590                });
591            }
592        }
593
594        // Check access list accounts
595        if tx.access_list.len() > MAX_ACCESS_LIST_ACCOUNTS {
596            return Err(TempoPoolTransactionError::TooManyAccessListAccounts {
597                count: tx.access_list.len(),
598                max_allowed: MAX_ACCESS_LIST_ACCOUNTS,
599            });
600        }
601
602        // Check storage keys per account and total
603        let mut total_storage_keys = 0usize;
604        for (idx, entry) in tx.access_list.iter().enumerate() {
605            if entry.storage_keys.len() > MAX_STORAGE_KEYS_PER_ACCOUNT {
606                return Err(TempoPoolTransactionError::TooManyStorageKeysPerAccount {
607                    account_index: idx,
608                    count: entry.storage_keys.len(),
609                    max_allowed: MAX_STORAGE_KEYS_PER_ACCOUNT,
610                });
611            }
612            total_storage_keys = total_storage_keys.saturating_add(entry.storage_keys.len());
613        }
614
615        if total_storage_keys > MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL {
616            return Err(TempoPoolTransactionError::TooManyTotalStorageKeys {
617                count: total_storage_keys,
618                max_allowed: MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL,
619            });
620        }
621
622        // Check token limits in key_authorization
623        if let Some(ref key_auth) = tx.key_authorization
624            && let Some(ref limits) = key_auth.limits
625            && limits.len() > MAX_TOKEN_LIMITS
626        {
627            return Err(TempoPoolTransactionError::TooManyTokenLimits {
628                count: limits.len(),
629                max_allowed: MAX_TOKEN_LIMITS,
630            });
631        }
632
633        Ok(())
634    }
635
636    /// Validates that a transaction's max_fee_per_gas is at least the minimum base fee
637    /// for the current hardfork.
638    ///
639    /// - T0: 10 billion attodollars minimum
640    /// - T1+: 20 billion attodollars minimum
641    fn ensure_min_base_fee(
642        &self,
643        transaction: &TempoPooledTransaction,
644        spec: TempoHardfork,
645    ) -> Result<(), TempoPoolTransactionError> {
646        let min_base_fee = spec.base_fee();
647        let max_fee_per_gas = transaction.max_fee_per_gas();
648
649        if max_fee_per_gas < min_base_fee as u128 {
650            return Err(TempoPoolTransactionError::FeeCapBelowMinBaseFee {
651                max_fee_per_gas,
652                min_base_fee,
653            });
654        }
655
656        Ok(())
657    }
658
659    fn validate_one(
660        &self,
661        origin: TransactionOrigin,
662        transaction: TempoPooledTransaction,
663        mut state_provider: impl StateProvider,
664    ) -> TransactionValidationOutcome<TempoPooledTransaction> {
665        // Get the current hardfork based on tip timestamp
666        let spec = self
667            .inner
668            .chain_spec()
669            .tempo_hardfork_at(self.inner.fork_tracker().tip_timestamp());
670
671        // Reject system transactions, those are never allowed in the pool.
672        if transaction.inner().is_system_tx() {
673            return TransactionValidationOutcome::Invalid(
674                transaction,
675                InvalidPoolTransactionError::Consensus(InvalidTransactionError::TxTypeNotSupported),
676            );
677        }
678
679        // Early reject oversized transactions before doing any expensive validation.
680        let tx_size = transaction.encoded_length();
681        let max_size = self.inner.max_tx_input_bytes();
682        if tx_size > max_size {
683            return TransactionValidationOutcome::Invalid(
684                transaction,
685                InvalidPoolTransactionError::OversizedData {
686                    size: tx_size,
687                    limit: max_size,
688                },
689            );
690        }
691
692        // Validate that max_fee_per_gas meets the minimum base fee for the current hardfork.
693        if let Err(err) = self.ensure_min_base_fee(&transaction, spec) {
694            return TransactionValidationOutcome::Invalid(
695                transaction,
696                InvalidPoolTransactionError::other(err),
697            );
698        }
699
700        // Validate keychain signature versions early so that permanently invalid
701        // errors before cheaper economic checks that would mask them.
702        if let Err(err) = self.validate_keychain_version(&transaction, spec) {
703            return TransactionValidationOutcome::Invalid(
704                transaction,
705                InvalidPoolTransactionError::other(err),
706            );
707        }
708
709        // Balance transfer is not allowed as there is no balances in accounts yet.
710        // Check added in https://github.com/tempoxyz/tempo/pull/759
711        // AATx will aggregate all call values, so we dont need additional check for AA transactions.
712        if !transaction.inner().value().is_zero() {
713            return TransactionValidationOutcome::Invalid(
714                transaction,
715                InvalidPoolTransactionError::other(TempoPoolTransactionError::NonZeroValue),
716            );
717        }
718
719        // Validate AA transaction temporal conditionals (`valid_before` and `valid_after`).
720        if let Some(tx) = transaction.inner().as_aa()
721            && let Err(err) = self.ensure_valid_conditionals(tx.tx())
722        {
723            return TransactionValidationOutcome::Invalid(
724                transaction,
725                InvalidPoolTransactionError::other(err),
726            );
727        }
728
729        // Validate AA transaction authorization list size.
730        if let Err(err) = self.ensure_authorization_list_size(&transaction) {
731            return TransactionValidationOutcome::Invalid(
732                transaction,
733                InvalidPoolTransactionError::other(err),
734            );
735        }
736
737        if transaction.inner().is_aa() {
738            // Validate AA transaction intrinsic gas.
739            // This ensures the gas limit covers all AA-specific costs (per-call overhead,
740            // signature verification, etc.) to prevent mempool DoS attacks where transactions
741            // pass pool validation but fail at execution time.
742            if let Err(err) = self.ensure_aa_intrinsic_gas(&transaction, spec, &state_provider) {
743                return TransactionValidationOutcome::Invalid(
744                    transaction,
745                    InvalidPoolTransactionError::other(err),
746                );
747            }
748        } else {
749            // validate intrinsic gas with additional TIP-1000 and T1 checks
750            if let Err(err) = ensure_intrinsic_gas_tempo_tx(&transaction, spec) {
751                return TransactionValidationOutcome::Invalid(transaction, err);
752            }
753        }
754
755        // Validate AA transaction field limits (calls, access list, token limits).
756        // This prevents DoS attacks via oversized transactions.
757        if let Err(err) = self.ensure_aa_field_limits(&transaction) {
758            return TransactionValidationOutcome::Invalid(
759                transaction,
760                InvalidPoolTransactionError::other(err),
761            );
762        }
763
764        let fee_payer = match transaction.inner().fee_payer(transaction.sender()) {
765            Ok(fee_payer) => fee_payer,
766            Err(_err) => {
767                return TransactionValidationOutcome::Invalid(
768                    transaction,
769                    InvalidPoolTransactionError::other(
770                        TempoPoolTransactionError::InvalidFeePayerSignature,
771                    ),
772                );
773            }
774        };
775
776        if transaction
777            .inner()
778            .as_aa()
779            .is_some_and(|aa| aa.tx().fee_payer_signature.is_some())
780            && fee_payer == transaction.sender()
781        {
782            return TransactionValidationOutcome::Invalid(
783                transaction,
784                InvalidPoolTransactionError::other(
785                    TempoPoolTransactionError::SelfSponsoredFeePayer,
786                ),
787            );
788        }
789
790        let fee_token = match state_provider.get_fee_token(transaction.inner(), fee_payer, spec) {
791            Ok(fee_token) => fee_token,
792            Err(err) => {
793                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
794            }
795        };
796
797        // Cache the resolved fee token for pool maintenance.
798        transaction.set_resolved_fee_token(fee_token);
799
800        // Ensure that fee token is valid.
801        match state_provider.is_valid_fee_token(spec, fee_token) {
802            Ok(valid) => {
803                if !valid {
804                    return TransactionValidationOutcome::Invalid(
805                        transaction,
806                        InvalidPoolTransactionError::other(
807                            TempoPoolTransactionError::InvalidFeeToken(fee_token),
808                        ),
809                    );
810                }
811            }
812            Err(err) => {
813                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
814            }
815        }
816
817        // Ensure that fee token is not paused.
818        match state_provider.is_fee_token_paused(spec, fee_token) {
819            Ok(paused) => {
820                if paused {
821                    return TransactionValidationOutcome::Invalid(
822                        transaction,
823                        InvalidPoolTransactionError::other(
824                            TempoPoolTransactionError::PausedFeeToken(fee_token),
825                        ),
826                    );
827                }
828            }
829            Err(err) => {
830                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
831            }
832        }
833
834        // Ensure that the fee payer is not blacklisted
835        match state_provider.can_fee_payer_transfer(fee_token, fee_payer, spec) {
836            Ok(valid) => {
837                if !valid {
838                    return TransactionValidationOutcome::Invalid(
839                        transaction,
840                        InvalidPoolTransactionError::other(
841                            TempoPoolTransactionError::BlackListedFeePayer {
842                                fee_token,
843                                fee_payer,
844                            },
845                        ),
846                    );
847                }
848            }
849            Err(err) => {
850                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
851            }
852        }
853
854        let balance = match state_provider.get_token_balance(fee_token, fee_payer, spec) {
855            Ok(balance) => balance,
856            Err(err) => {
857                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
858            }
859        };
860
861        // Get the tx cost and adjust for fee token decimals
862        let cost = transaction.fee_token_cost();
863        if balance < cost {
864            return TransactionValidationOutcome::Invalid(
865                transaction,
866                InvalidTransactionError::InsufficientFunds(
867                    GotExpected {
868                        got: balance,
869                        expected: cost,
870                    }
871                    .into(),
872                )
873                .into(),
874            );
875        }
876
877        match self
878            .amm_liquidity_cache
879            .has_enough_liquidity(fee_token, cost, &state_provider)
880        {
881            Ok(true) => {}
882            Ok(false) => {
883                return TransactionValidationOutcome::Invalid(
884                    transaction,
885                    InvalidPoolTransactionError::other(
886                        TempoPoolTransactionError::InsufficientLiquidity(fee_token),
887                    ),
888                );
889            }
890            Err(err) => {
891                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
892            }
893        }
894
895        // Validate transactions that involve keychain keys.
896        match self.validate_against_keychain(
897            &transaction,
898            &mut state_provider,
899            fee_payer,
900            fee_token,
901        ) {
902            Ok(Ok(())) => {}
903            Ok(Err(err)) => {
904                return TransactionValidationOutcome::Invalid(
905                    transaction,
906                    InvalidPoolTransactionError::other(err),
907                );
908            }
909            Err(err) => {
910                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
911            }
912        }
913
914        match self
915            .inner
916            .validate_one_with_state_provider(origin, transaction, &state_provider)
917        {
918            TransactionValidationOutcome::Valid {
919                balance,
920                mut state_nonce,
921                bytecode_hash,
922                transaction,
923                propagate,
924                authorities,
925            } => {
926                // Additional nonce validations for non-protocol nonce keys
927                if let Some(nonce_key) = transaction.transaction().nonce_key()
928                    && !nonce_key.is_zero()
929                {
930                    // ensure the nonce key isn't prefixed with the sub-block prefix
931                    if has_sub_block_nonce_key_prefix(&nonce_key) {
932                        return TransactionValidationOutcome::Invalid(
933                            transaction.into_transaction(),
934                            InvalidPoolTransactionError::other(
935                                TempoPoolTransactionError::SubblockNonceKey,
936                            ),
937                        );
938                    }
939
940                    // Check if T1 hardfork is active for expiring nonce handling
941                    let current_time = self.inner.fork_tracker().tip_timestamp();
942                    let is_t1_active = self
943                        .inner
944                        .chain_spec()
945                        .is_t1_active_at_timestamp(current_time);
946
947                    if is_t1_active && nonce_key == TEMPO_EXPIRING_NONCE_KEY {
948                        // Expiring nonce transaction - check if the replay hash is already seen.
949                        //
950                        // Pre-T1B: use tx_hash to match handler behavior (handler writes seen[tx_hash]).
951                        // T1B+: use expiring_nonce_hash (invariant to fee payer changes) to match
952                        //        the updated handler replay protection.
953                        //
954                        // TODO: Remove the tx_hash path after T1B is active on mainnet.
955                        let replay_hash = if spec.is_t1b() {
956                            transaction
957                                .transaction()
958                                .inner()
959                                .as_aa()
960                                .expect("expiring nonce tx must be AA")
961                                .expiring_nonce_hash(transaction.transaction().sender())
962                        } else {
963                            *transaction.hash()
964                        };
965
966                        // If the replay hash is still active (seen and not expired), reject.
967                        // Note: This is also enforced at the protocol level in handler.rs via
968                        // `check_and_mark_expiring_nonce`, so even if a tx bypasses pool validation
969                        // (e.g., injected directly into a block), execution will still reject it.
970                        match state_provider.with_read_only_storage_ctx(spec, || {
971                            NonceManager::new().is_expiring_nonce_seen(replay_hash, current_time)
972                        }) {
973                            Err(err) => {
974                                return TransactionValidationOutcome::Error(
975                                    *transaction.hash(),
976                                    Box::new(err),
977                                );
978                            }
979                            Ok(true) => {
980                                return TransactionValidationOutcome::Invalid(
981                                    transaction.into_transaction(),
982                                    InvalidPoolTransactionError::other(
983                                        TempoPoolTransactionError::ExpiringNonceReplay,
984                                    ),
985                                );
986                            }
987                            Ok(false) => (),
988                        };
989                    } else {
990                        // This is a 2D nonce transaction - validate against 2D nonce
991                        state_nonce = match state_provider.with_read_only_storage_ctx(spec, || {
992                            NonceManager::new().get_nonce(INonce::getNonceCall {
993                                account: transaction.transaction().sender(),
994                                nonceKey: nonce_key,
995                            })
996                        }) {
997                            Ok(nonce) => nonce,
998                            Err(err) => {
999                                return TransactionValidationOutcome::Error(
1000                                    *transaction.hash(),
1001                                    Box::new(err),
1002                                );
1003                            }
1004                        };
1005                        let tx_nonce = transaction.nonce();
1006                        if tx_nonce < state_nonce {
1007                            return TransactionValidationOutcome::Invalid(
1008                                transaction.into_transaction(),
1009                                InvalidTransactionError::NonceNotConsistent {
1010                                    tx: tx_nonce,
1011                                    state: state_nonce,
1012                                }
1013                                .into(),
1014                            );
1015                        }
1016                    }
1017                }
1018
1019                // Pre-compute TempoTxEnv to avoid the cost during payload building.
1020                transaction.transaction().prepare_tx_env();
1021
1022                TransactionValidationOutcome::Valid {
1023                    balance,
1024                    state_nonce,
1025                    bytecode_hash,
1026                    transaction,
1027                    propagate,
1028                    authorities,
1029                }
1030            }
1031            outcome => outcome,
1032        }
1033    }
1034}
1035
1036impl<Client> TransactionValidator for TempoTransactionValidator<Client>
1037where
1038    Client: ChainSpecProvider<ChainSpec = TempoChainSpec> + StateProviderFactory,
1039{
1040    type Transaction = TempoPooledTransaction;
1041    type Block = Block;
1042
1043    async fn validate_transaction(
1044        &self,
1045        origin: TransactionOrigin,
1046        transaction: Self::Transaction,
1047    ) -> TransactionValidationOutcome<Self::Transaction> {
1048        let state_provider = match self.inner.client().latest() {
1049            Ok(provider) => provider,
1050            Err(err) => {
1051                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
1052            }
1053        };
1054
1055        self.validate_one(origin, transaction, state_provider)
1056    }
1057
1058    async fn validate_transactions(
1059        &self,
1060        transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction), IntoIter: Send>
1061        + Send,
1062    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
1063        let transactions: Vec<_> = transactions.into_iter().collect();
1064        let state_provider = match self.inner.client().latest() {
1065            Ok(provider) => provider,
1066            Err(err) => {
1067                return transactions
1068                    .into_iter()
1069                    .map(|(_, tx)| {
1070                        TransactionValidationOutcome::Error(*tx.hash(), Box::new(err.clone()))
1071                    })
1072                    .collect();
1073            }
1074        };
1075
1076        transactions
1077            .into_iter()
1078            .map(|(origin, tx)| self.validate_one(origin, tx, &state_provider))
1079            .collect()
1080    }
1081
1082    async fn validate_transactions_with_origin(
1083        &self,
1084        origin: TransactionOrigin,
1085        transactions: impl IntoIterator<Item = Self::Transaction> + Send,
1086    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
1087        let state_provider = match self.inner.client().latest() {
1088            Ok(provider) => provider,
1089            Err(err) => {
1090                return transactions
1091                    .into_iter()
1092                    .map(|tx| {
1093                        TransactionValidationOutcome::Error(*tx.hash(), Box::new(err.clone()))
1094                    })
1095                    .collect();
1096            }
1097        };
1098
1099        transactions
1100            .into_iter()
1101            .map(|tx| self.validate_one(origin, tx, &state_provider))
1102            .collect()
1103    }
1104
1105    fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
1106        self.inner.on_new_head_block(new_tip_block)
1107    }
1108}
1109
1110/// Ensures that gas limit of the transaction exceeds the intrinsic gas of the transaction.
1111pub fn ensure_intrinsic_gas_tempo_tx(
1112    tx: &TempoPooledTransaction,
1113    spec: TempoHardfork,
1114) -> Result<(), InvalidPoolTransactionError> {
1115    let gas_params = tempo_gas_params(spec);
1116
1117    let mut gas = gas_params.initial_tx_gas(
1118        tx.input(),
1119        tx.is_create(),
1120        tx.access_list().map(|l| l.len()).unwrap_or_default() as u64,
1121        tx.access_list()
1122            .map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
1123            .unwrap_or_default() as u64,
1124        tx.authorization_list().map(|l| l.len()).unwrap_or_default() as u64,
1125    );
1126
1127    // TIP-1000: Storage pricing updates for launch
1128    // EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas.
1129    // no need for v1 fork check as gas_params would be zero
1130    for auth in tx.authorization_list().unwrap_or_default() {
1131        if auth.nonce == 0 {
1132            gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost();
1133        }
1134    }
1135
1136    // TIP-1000: Storage pricing updates for launch
1137    // Tempo transactions with `nonce == 0` require additional gas, but the amount depends on nonce type:
1138    // - Expiring nonce (nonce_key == MAX): EXPIRING_NONCE_GAS (13k) for ring buffer operations
1139    // - Regular/2D nonce with nonce == 0: new_account_cost (250k) for potential account creation
1140    if spec.is_t1() && tx.nonce() == 0 {
1141        if tx.nonce_key() == Some(TEMPO_EXPIRING_NONCE_KEY) {
1142            gas.initial_gas += EXPIRING_NONCE_GAS;
1143        } else {
1144            gas.initial_gas += gas_params.get(GasId::new_account_cost());
1145        }
1146    }
1147
1148    let gas_limit = tx.gas_limit();
1149    if gas_limit < gas.initial_gas || gas_limit < gas.floor_gas {
1150        Err(InvalidPoolTransactionError::IntrinsicGasTooLow)
1151    } else {
1152        Ok(())
1153    }
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158    use super::*;
1159    use crate::{test_utils::TxBuilder, transaction::TempoPoolTransactionError};
1160    use alloy_consensus::{Header, Signed, Transaction, TxLegacy};
1161    use alloy_primitives::{Address, B256, TxKind, U256, address, uint};
1162    use alloy_signer::Signature;
1163    use reth_primitives_traits::SignedTransaction;
1164    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
1165    use reth_transaction_pool::{
1166        PoolTransaction, blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder,
1167    };
1168    use std::sync::Arc;
1169    use tempo_chainspec::spec::{MODERATO, TEMPO_T1_TX_GAS_LIMIT_CAP};
1170    use tempo_precompiles::{
1171        PATH_USD_ADDRESS, TIP403_REGISTRY_ADDRESS,
1172        tip20::{TIP20Token, slots as tip20_slots},
1173        tip403_registry::{ITIP403Registry, PolicyData, TIP403Registry},
1174    };
1175    use tempo_primitives::{
1176        Block, TempoHeader, TempoPrimitives, TempoTxEnvelope,
1177        transaction::{
1178            TempoTransaction,
1179            envelope::TEMPO_SYSTEM_TX_SIGNATURE,
1180            tempo_transaction::Call,
1181            tt_signature::{PrimitiveSignature, TempoSignature},
1182            tt_signed::AASigned,
1183        },
1184    };
1185    use tempo_revm::TempoStateAccess;
1186
1187    /// Arbitrary validity window (in seconds) used for expiring-nonce transactions in tests.
1188    const TEST_VALIDITY_WINDOW: u64 = 25;
1189
1190    /// Helper to create a mock sealed block with the given timestamp.
1191    fn create_mock_block(timestamp: u64) -> SealedBlock<Block> {
1192        let header = TempoHeader {
1193            inner: Header {
1194                timestamp,
1195                gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1196                ..Default::default()
1197            },
1198            ..Default::default()
1199        };
1200        let block = Block {
1201            header,
1202            body: Default::default(),
1203        };
1204        SealedBlock::seal_slow(block)
1205    }
1206
1207    /// Helper function to create an AA transaction with the given `valid_after` and `valid_before`
1208    /// timestamps
1209    fn create_aa_transaction(
1210        valid_after: Option<u64>,
1211        valid_before: Option<u64>,
1212    ) -> TempoPooledTransaction {
1213        let mut builder = TxBuilder::aa(Address::random())
1214            .fee_token(address!("0000000000000000000000000000000000000002"));
1215        if let Some(va) = valid_after {
1216            builder = builder.valid_after(va);
1217        }
1218        if let Some(vb) = valid_before {
1219            builder = builder.valid_before(vb);
1220        }
1221        builder.build()
1222    }
1223
1224    /// Helper function to setup validator with the given transaction and tip timestamp.
1225    fn setup_validator(
1226        transaction: &TempoPooledTransaction,
1227        tip_timestamp: u64,
1228    ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
1229        let provider = MockEthProvider::<TempoPrimitives>::new()
1230            .with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
1231        provider.add_account(
1232            transaction.sender(),
1233            ExtendedAccount::new(transaction.nonce(), alloy_primitives::U256::ZERO),
1234        );
1235        let block_with_gas = Block {
1236            header: TempoHeader {
1237                inner: Header {
1238                    gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1239                    ..Default::default()
1240                },
1241                ..Default::default()
1242            },
1243            ..Default::default()
1244        };
1245        provider.add_block(B256::random(), block_with_gas);
1246
1247        // Setup PATH_USD as a valid fee token with USD currency and always-allow transfer policy
1248        // USD_CURRENCY_SLOT_VALUE: "USD" left-padded with length marker (3 bytes * 2 = 6)
1249        let usd_currency_value =
1250            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
1251        // transfer_policy_id is packed at byte offset 20 in slot 7, so we need to shift
1252        // policy_id=1 left by 160 bits (20 * 8) to position it correctly
1253        let transfer_policy_id_packed =
1254            uint!(0x0000000000000000000000010000000000000000000000000000000000000000_U256);
1255        // Compute the balance slot for the sender in the PATH_USD token
1256        let balance_slot = TIP20Token::from_address(PATH_USD_ADDRESS)
1257            .expect("PATH_USD_ADDRESS is a valid TIP20 token")
1258            .balances[transaction.sender()]
1259        .slot();
1260        // Give the sender enough balance to cover the transaction cost
1261        let fee_payer_balance = U256::from(1_000_000_000_000u64); // 1M USD in 6 decimals
1262        provider.add_account(
1263            PATH_USD_ADDRESS,
1264            ExtendedAccount::new(0, U256::ZERO).extend_storage([
1265                (tip20_slots::CURRENCY.into(), usd_currency_value),
1266                (
1267                    tip20_slots::TRANSFER_POLICY_ID.into(),
1268                    transfer_policy_id_packed,
1269                ),
1270                (balance_slot.into(), fee_payer_balance),
1271            ]),
1272        );
1273
1274        let inner =
1275            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
1276                .disable_balance_check()
1277                .build(InMemoryBlobStore::default());
1278        let amm_cache =
1279            AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
1280        let validator = TempoTransactionValidator::new(
1281            inner,
1282            DEFAULT_AA_VALID_AFTER_MAX_SECS,
1283            DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1284            amm_cache,
1285        );
1286
1287        // Set the tip timestamp by simulating a new head block
1288        let mock_block = create_mock_block(tip_timestamp);
1289        validator.on_new_head_block(&mock_block);
1290
1291        validator
1292    }
1293
1294    #[tokio::test]
1295    async fn test_some_balance() {
1296        let transaction = TxBuilder::eip1559(Address::random())
1297            .value(U256::from(1))
1298            .build_eip1559();
1299        let validator = setup_validator(&transaction, 0);
1300
1301        let outcome = validator
1302            .validate_transaction(TransactionOrigin::External, transaction.clone())
1303            .await;
1304
1305        match outcome {
1306            TransactionValidationOutcome::Invalid(_, ref err) => {
1307                assert!(matches!(
1308                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1309                    Some(TempoPoolTransactionError::NonZeroValue)
1310                ));
1311            }
1312            _ => panic!("Expected Invalid outcome with NonZeroValue error, got: {outcome:?}"),
1313        }
1314    }
1315
1316    #[tokio::test]
1317    async fn test_system_tx_rejected_as_invalid() {
1318        let tx = TxLegacy {
1319            chain_id: Some(MODERATO.chain_id()),
1320            nonce: 0,
1321            gas_price: 0,
1322            gas_limit: 0,
1323            to: TxKind::Call(Address::ZERO),
1324            value: U256::ZERO,
1325            input: Default::default(),
1326        };
1327        let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1328        let transaction = TempoPooledTransaction::new(
1329            reth_primitives_traits::Recovered::new_unchecked(envelope, Address::ZERO),
1330        );
1331        let validator = setup_validator(&transaction, 0);
1332
1333        let outcome = validator
1334            .validate_transaction(TransactionOrigin::External, transaction)
1335            .await;
1336
1337        match outcome {
1338            TransactionValidationOutcome::Invalid(_, err) => {
1339                assert!(matches!(
1340                    err,
1341                    InvalidPoolTransactionError::Consensus(
1342                        InvalidTransactionError::TxTypeNotSupported
1343                    )
1344                ));
1345            }
1346            _ => panic!("Expected Invalid outcome with TxTypeNotSupported error, got: {outcome:?}"),
1347        }
1348    }
1349
1350    #[tokio::test]
1351    async fn test_invalid_fee_payer_signature_rejected() {
1352        let calls: Vec<Call> = vec![Call {
1353            to: TxKind::Call(Address::random()),
1354            value: U256::ZERO,
1355            input: Default::default(),
1356        }];
1357
1358        let tx = TempoTransaction {
1359            chain_id: MODERATO.chain_id(),
1360            max_priority_fee_per_gas: 1_000_000_000,
1361            max_fee_per_gas: 20_000_000_000,
1362            gas_limit: 1_000_000,
1363            calls,
1364            nonce_key: U256::ZERO,
1365            nonce: 0,
1366            fee_token: Some(PATH_USD_ADDRESS),
1367            fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
1368            ..Default::default()
1369        };
1370
1371        let signed = AASigned::new_unhashed(
1372            tx,
1373            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1374        );
1375        let transaction = TempoPooledTransaction::new(
1376            TempoTxEnvelope::from(signed).try_into_recovered().unwrap(),
1377        );
1378        let validator = setup_validator(&transaction, 0);
1379
1380        let outcome = validator
1381            .validate_transaction(TransactionOrigin::External, transaction)
1382            .await;
1383
1384        match outcome {
1385            TransactionValidationOutcome::Invalid(_, ref err) => {
1386                assert!(matches!(
1387                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1388                    Some(TempoPoolTransactionError::InvalidFeePayerSignature)
1389                ));
1390            }
1391            _ => panic!(
1392                "Expected Invalid outcome with InvalidFeePayerSignature error, got: {outcome:?}"
1393            ),
1394        }
1395    }
1396
1397    #[tokio::test]
1398    async fn test_self_sponsored_fee_payer_rejected() {
1399        use alloy_signer::SignerSync;
1400        use alloy_signer_local::PrivateKeySigner;
1401
1402        let signer = PrivateKeySigner::random();
1403        let sender = signer.address();
1404
1405        let mut tx = TempoTransaction {
1406            chain_id: MODERATO.chain_id(),
1407            max_priority_fee_per_gas: 1_000_000_000,
1408            max_fee_per_gas: 20_000_000_000,
1409            gas_limit: 1_000_000,
1410            calls: vec![Call {
1411                to: TxKind::Call(Address::random()),
1412                value: U256::ZERO,
1413                input: Default::default(),
1414            }],
1415            nonce_key: U256::ZERO,
1416            nonce: 0,
1417            fee_token: Some(PATH_USD_ADDRESS),
1418            fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
1419            ..Default::default()
1420        };
1421
1422        let fee_payer_hash = tx.fee_payer_signature_hash(sender);
1423        tx.fee_payer_signature = Some(
1424            signer
1425                .sign_hash_sync(&fee_payer_hash)
1426                .expect("fee payer signing should succeed"),
1427        );
1428
1429        let signed = AASigned::new_unhashed(
1430            tx,
1431            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1432        );
1433
1434        let envelope: TempoTxEnvelope = signed.into();
1435        let transaction = TempoPooledTransaction::new(
1436            reth_primitives_traits::Recovered::new_unchecked(envelope, sender),
1437        );
1438        let validator = setup_validator(&transaction, 0);
1439
1440        let outcome = validator
1441            .validate_transaction(TransactionOrigin::External, transaction)
1442            .await;
1443
1444        match outcome {
1445            TransactionValidationOutcome::Invalid(_, ref err) => {
1446                assert!(matches!(
1447                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1448                    Some(TempoPoolTransactionError::SelfSponsoredFeePayer)
1449                ));
1450            }
1451            _ => panic!(
1452                "Expected Invalid outcome with SelfSponsoredFeePayer error, got: {outcome:?}"
1453            ),
1454        }
1455    }
1456
1457    #[tokio::test]
1458    async fn test_aa_valid_before_check() {
1459        // NOTE: `setup_validator` will turn `tip_timestamp` into `current_time`
1460        let current_time = std::time::SystemTime::now()
1461            .duration_since(std::time::UNIX_EPOCH)
1462            .unwrap()
1463            .as_secs();
1464
1465        // Test case 1: No `valid_before`
1466        let tx_no_valid_before = create_aa_transaction(None, None);
1467        let validator = setup_validator(&tx_no_valid_before, current_time);
1468        let outcome = validator
1469            .validate_transaction(TransactionOrigin::External, tx_no_valid_before)
1470            .await;
1471
1472        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1473            assert!(!matches!(
1474                err.downcast_other_ref::<TempoPoolTransactionError>(),
1475                Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1476            ));
1477        }
1478
1479        // Test case 2: `valid_before` too small (at boundary)
1480        let tx_too_close =
1481            create_aa_transaction(None, Some(current_time + AA_VALID_BEFORE_MIN_SECS));
1482        let validator = setup_validator(&tx_too_close, current_time);
1483        let outcome = validator
1484            .validate_transaction(TransactionOrigin::External, tx_too_close)
1485            .await;
1486
1487        match outcome {
1488            TransactionValidationOutcome::Invalid(_, ref err) => {
1489                assert!(matches!(
1490                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1491                    Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1492                ));
1493            }
1494            _ => panic!("Expected Invalid outcome with InvalidValidBefore error, got: {outcome:?}"),
1495        }
1496
1497        // Test case 3: `valid_before` sufficiently in the future
1498        let tx_valid =
1499            create_aa_transaction(None, Some(current_time + AA_VALID_BEFORE_MIN_SECS + 1));
1500        let validator = setup_validator(&tx_valid, current_time);
1501        let outcome = validator
1502            .validate_transaction(TransactionOrigin::External, tx_valid)
1503            .await;
1504
1505        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1506            assert!(!matches!(
1507                err.downcast_other_ref::<TempoPoolTransactionError>(),
1508                Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1509            ));
1510        }
1511    }
1512
1513    #[tokio::test]
1514    async fn test_aa_valid_after_check() {
1515        // NOTE: `setup_validator` will turn `tip_timestamp` into `current_time`
1516        let current_time = std::time::SystemTime::now()
1517            .duration_since(std::time::UNIX_EPOCH)
1518            .unwrap()
1519            .as_secs();
1520
1521        // Test case 1: No `valid_after`
1522        let tx_no_valid_after = create_aa_transaction(None, None);
1523        let validator = setup_validator(&tx_no_valid_after, current_time);
1524        let outcome = validator
1525            .validate_transaction(TransactionOrigin::External, tx_no_valid_after)
1526            .await;
1527
1528        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1529            assert!(!matches!(
1530                err.downcast_other_ref::<TempoPoolTransactionError>(),
1531                Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1532            ));
1533        }
1534
1535        // Test case 2: `valid_after` within limit (60 seconds)
1536        let tx_within_limit = create_aa_transaction(Some(current_time + 60), None);
1537        let validator = setup_validator(&tx_within_limit, current_time);
1538        let outcome = validator
1539            .validate_transaction(TransactionOrigin::External, tx_within_limit)
1540            .await;
1541
1542        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1543            assert!(!matches!(
1544                err.downcast_other_ref::<TempoPoolTransactionError>(),
1545                Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1546            ));
1547        }
1548
1549        // Test case 3: `valid_after` beyond limit (5 minutes, exceeds 120s max)
1550        let tx_too_far = create_aa_transaction(Some(current_time + 300), None);
1551        let validator = setup_validator(&tx_too_far, current_time);
1552        let outcome = validator
1553            .validate_transaction(TransactionOrigin::External, tx_too_far)
1554            .await;
1555
1556        match outcome {
1557            TransactionValidationOutcome::Invalid(_, ref err) => {
1558                assert!(matches!(
1559                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1560                    Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1561                ));
1562            }
1563            _ => panic!("Expected Invalid outcome with InvalidValidAfter error, got: {outcome:?}"),
1564        }
1565    }
1566
1567    #[tokio::test]
1568    async fn test_blacklisted_fee_payer_rejected() {
1569        // Use a valid TIP20 token address (PATH_USD with token_id=1)
1570        let fee_token = address!("20C0000000000000000000000000000000000001");
1571        let policy_id: u64 = 2;
1572
1573        let transaction = TxBuilder::aa(Address::random())
1574            .fee_token(fee_token)
1575            .build();
1576        let fee_payer = transaction.sender();
1577
1578        // Setup provider with storage
1579        let provider = MockEthProvider::<TempoPrimitives>::new()
1580            .with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
1581        provider.add_block(B256::random(), Block::default());
1582
1583        // Add sender account
1584        provider.add_account(
1585            transaction.sender(),
1586            ExtendedAccount::new(transaction.nonce(), U256::ZERO),
1587        );
1588
1589        // Add TIP20 token with transfer_policy_id pointing to blacklist policy
1590        // USD_CURRENCY_SLOT_VALUE: "USD" left-padded with length marker (3 bytes * 2 = 6)
1591        let usd_currency_value =
1592            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
1593        // transfer_policy_id is packed at byte offset 20 in slot 7, so we need to shift
1594        // policy_id left by TRANSFER_POLICY_ID_OFFSET bits to position it correctly
1595        let transfer_policy_id_packed =
1596            U256::from(policy_id) << tip20_slots::TRANSFER_POLICY_ID_OFFSET;
1597        provider.add_account(
1598            fee_token,
1599            ExtendedAccount::new(0, U256::ZERO).extend_storage([
1600                (
1601                    tip20_slots::TRANSFER_POLICY_ID.into(),
1602                    transfer_policy_id_packed,
1603                ),
1604                (tip20_slots::CURRENCY.into(), usd_currency_value),
1605            ]),
1606        );
1607
1608        // Add TIP403Registry with blacklist policy containing fee_payer
1609        let policy_data = PolicyData {
1610            policy_type: ITIP403Registry::PolicyType::BLACKLIST as u8,
1611            admin: Address::ZERO,
1612        };
1613        let policy_data_slot = TIP403Registry::new().policy_records[policy_id]
1614            .base
1615            .base_slot();
1616        let policy_set_slot = TIP403Registry::new().policy_set[policy_id][fee_payer].slot();
1617
1618        provider.add_account(
1619            TIP403_REGISTRY_ADDRESS,
1620            ExtendedAccount::new(0, U256::ZERO).extend_storage([
1621                (policy_data_slot.into(), policy_data.encode_to_slot()),
1622                (policy_set_slot.into(), U256::from(1)), // in blacklist = true
1623            ]),
1624        );
1625
1626        // Create validator and validate
1627        let inner =
1628            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
1629                .disable_balance_check()
1630                .build(InMemoryBlobStore::default());
1631        let validator = TempoTransactionValidator::new(
1632            inner,
1633            DEFAULT_AA_VALID_AFTER_MAX_SECS,
1634            DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1635            AmmLiquidityCache::new(provider).unwrap(),
1636        );
1637
1638        let outcome = validator
1639            .validate_transaction(TransactionOrigin::External, transaction)
1640            .await;
1641
1642        // Assert BlackListedFeePayer error
1643        match outcome {
1644            TransactionValidationOutcome::Invalid(_, ref err) => {
1645                assert!(matches!(
1646                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1647                    Some(TempoPoolTransactionError::BlackListedFeePayer { .. })
1648                ));
1649            }
1650            _ => {
1651                panic!("Expected Invalid outcome with BlackListedFeePayer error, got: {outcome:?}")
1652            }
1653        }
1654    }
1655
1656    /// Test AA intrinsic gas validation rejects insufficient gas and accepts sufficient gas.
1657    /// This is the fix for the audit finding about mempool DoS via gas calculation mismatch.
1658    #[tokio::test]
1659    async fn test_aa_intrinsic_gas_validation() {
1660        use alloy_primitives::{Signature, TxKind, address};
1661        use tempo_primitives::transaction::{
1662            TempoTransaction,
1663            tempo_transaction::Call,
1664            tt_signature::{PrimitiveSignature, TempoSignature},
1665            tt_signed::AASigned,
1666        };
1667
1668        let current_time = std::time::SystemTime::now()
1669            .duration_since(std::time::UNIX_EPOCH)
1670            .unwrap()
1671            .as_secs();
1672
1673        // Helper to create AA tx with given gas limit
1674        let create_aa_tx = |gas_limit: u64| {
1675            let calls: Vec<Call> = (0..10)
1676                .map(|i| Call {
1677                    to: TxKind::Call(Address::from([i as u8; 20])),
1678                    value: U256::ZERO,
1679                    input: alloy_primitives::Bytes::from(vec![0x00; 100]),
1680                })
1681                .collect();
1682
1683            let tx = TempoTransaction {
1684                chain_id: 1,
1685                max_priority_fee_per_gas: 1_000_000_000,
1686                max_fee_per_gas: 20_000_000_000, // 20 gwei, above T1's minimum
1687                gas_limit,
1688                calls,
1689                nonce_key: U256::ZERO,
1690                nonce: 0,
1691                fee_token: Some(address!("0000000000000000000000000000000000000002")),
1692                ..Default::default()
1693            };
1694
1695            let signed = AASigned::new_unhashed(
1696                tx,
1697                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1698                    Signature::test_signature(),
1699                )),
1700            );
1701            TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1702        };
1703
1704        // Intrinsic gas for 10 calls: 21k base + 10*2600 cold access + 10*100*4 calldata = ~51k
1705        // Test 1: 30k gas should be rejected
1706        let tx_low_gas = create_aa_tx(30_000);
1707        let validator = setup_validator(&tx_low_gas, current_time);
1708        let outcome = validator
1709            .validate_transaction(TransactionOrigin::External, tx_low_gas)
1710            .await;
1711
1712        match outcome {
1713            TransactionValidationOutcome::Invalid(_, ref err) => {
1714                assert!(matches!(
1715                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1716                    Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1717                ));
1718            }
1719            _ => panic!(
1720                "Expected Invalid outcome with InsufficientGasForAAIntrinsicCost, got: {outcome:?}"
1721            ),
1722        }
1723
1724        // Test 2: 1M gas should pass intrinsic gas check
1725        let tx_high_gas = create_aa_tx(1_000_000);
1726        let validator = setup_validator(&tx_high_gas, current_time);
1727        let outcome = validator
1728            .validate_transaction(TransactionOrigin::External, tx_high_gas)
1729            .await;
1730
1731        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1732            assert!(!matches!(
1733                err.downcast_other_ref::<TempoPoolTransactionError>(),
1734                Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1735            ));
1736        }
1737    }
1738
1739    /// Test that CREATE transactions with 2D nonce (nonce_key != 0) require additional gas
1740    /// when the sender's account nonce is 0 (account creation cost).
1741    ///
1742    /// The new logic adds 250k gas requirement when:
1743    /// - Transaction has 2D nonce (nonce_key != 0)
1744    /// - Transaction is CREATE
1745    /// - Account nonce is 0
1746    #[tokio::test]
1747    async fn test_aa_create_tx_with_2d_nonce_intrinsic_gas() {
1748        use alloy_primitives::Signature;
1749        use tempo_primitives::transaction::{
1750            TempoTransaction,
1751            tempo_transaction::Call as TxCall,
1752            tt_signature::{PrimitiveSignature, TempoSignature},
1753            tt_signed::AASigned,
1754        };
1755
1756        let current_time = std::time::SystemTime::now()
1757            .duration_since(std::time::UNIX_EPOCH)
1758            .unwrap()
1759            .as_secs();
1760
1761        // Helper to create AA transaction
1762        let create_aa_tx = |gas_limit: u64, nonce_key: U256, is_create: bool| {
1763            let calls: Vec<TxCall> = if is_create {
1764                vec![TxCall {
1765                    to: TxKind::Create,
1766                    value: U256::ZERO,
1767                    input: alloy_primitives::Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xF3]),
1768                }]
1769            } else {
1770                (0..10)
1771                    .map(|i| TxCall {
1772                        to: TxKind::Call(Address::from([i as u8; 20])),
1773                        value: U256::ZERO,
1774                        input: alloy_primitives::Bytes::from(vec![0x00; 100]),
1775                    })
1776                    .collect()
1777            };
1778
1779            let valid_before = if nonce_key == TEMPO_EXPIRING_NONCE_KEY {
1780                Some(current_time + TEST_VALIDITY_WINDOW)
1781            } else {
1782                None
1783            };
1784
1785            let tx = TempoTransaction {
1786                chain_id: 1,
1787                max_priority_fee_per_gas: 1_000_000_000,
1788                max_fee_per_gas: 20_000_000_000,
1789                gas_limit,
1790                calls,
1791                nonce_key,
1792                nonce: 0,
1793                valid_before,
1794                fee_token: Some(address!("0000000000000000000000000000000000000002")),
1795                ..Default::default()
1796            };
1797
1798            let signed = AASigned::new_unhashed(
1799                tx,
1800                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1801                    Signature::test_signature(),
1802                )),
1803            );
1804            TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1805        };
1806
1807        // Test 1: Verify 1D nonce (nonce_key=0) with low gas fails intrinsic gas check
1808        let tx_1d_low_gas = create_aa_tx(30_000, U256::ZERO, false);
1809        let validator1 = setup_validator(&tx_1d_low_gas, current_time);
1810        let outcome1 = validator1
1811            .validate_transaction(TransactionOrigin::External, tx_1d_low_gas)
1812            .await;
1813
1814        match outcome1 {
1815            TransactionValidationOutcome::Invalid(_, ref err) => {
1816                assert!(
1817                    matches!(
1818                        err.downcast_other_ref::<TempoPoolTransactionError>(),
1819                        Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1820                    ),
1821                    "1D nonce with low gas should fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1822                );
1823            }
1824            _ => panic!("Expected Invalid outcome, got: {outcome1:?}"),
1825        }
1826
1827        // Test 2: Verify 2D nonce (nonce_key != 0) with same low gas also fails intrinsic gas check
1828        // This confirms that 2D nonce adds additional gas requirements (for nonce == 0 case)
1829        let tx_2d_low_gas = create_aa_tx(30_000, TEMPO_EXPIRING_NONCE_KEY, false);
1830        let validator2 = setup_validator(&tx_2d_low_gas, current_time);
1831        let outcome2 = validator2
1832            .validate_transaction(TransactionOrigin::External, tx_2d_low_gas)
1833            .await;
1834
1835        match outcome2 {
1836            TransactionValidationOutcome::Invalid(_, ref err) => {
1837                assert!(
1838                    matches!(
1839                        err.downcast_other_ref::<TempoPoolTransactionError>(),
1840                        Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1841                    ),
1842                    "2D nonce with low gas should fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1843                );
1844            }
1845            _ => panic!("Expected Invalid outcome, got: {outcome2:?}"),
1846        }
1847
1848        // Test 3: 1D nonce with sufficient gas should NOT fail intrinsic gas check
1849        let tx_1d_high_gas = create_aa_tx(1_000_000, U256::ZERO, false);
1850        let validator3 = setup_validator(&tx_1d_high_gas, current_time);
1851        let outcome3 = validator3
1852            .validate_transaction(TransactionOrigin::External, tx_1d_high_gas)
1853            .await;
1854
1855        // May fail for other reasons (fee token, etc.) but should NOT fail intrinsic gas
1856        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome3 {
1857            assert!(
1858                !matches!(
1859                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1860                    Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1861                ),
1862                "1D nonce with high gas should NOT fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1863            );
1864        }
1865
1866        // Test 4: 2D nonce with sufficient gas should NOT fail intrinsic gas check
1867        let tx_2d_high_gas = create_aa_tx(1_000_000, TEMPO_EXPIRING_NONCE_KEY, false);
1868        let validator4 = setup_validator(&tx_2d_high_gas, current_time);
1869        let outcome4 = validator4
1870            .validate_transaction(TransactionOrigin::External, tx_2d_high_gas)
1871            .await;
1872
1873        // May fail for other reasons (fee token, etc.) but should NOT fail intrinsic gas
1874        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome4 {
1875            assert!(
1876                !matches!(
1877                    err.downcast_other_ref::<TempoPoolTransactionError>(),
1878                    Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1879                ),
1880                "2D nonce with high gas should NOT fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1881            );
1882        }
1883    }
1884
1885    #[tokio::test]
1886    async fn test_expiring_nonce_intrinsic_gas_uses_lower_cost() {
1887        use alloy_primitives::{Signature, TxKind, address};
1888        use tempo_primitives::transaction::{
1889            TempoTransaction,
1890            tempo_transaction::Call,
1891            tt_signature::{PrimitiveSignature, TempoSignature},
1892            tt_signed::AASigned,
1893        };
1894
1895        let current_time = std::time::SystemTime::now()
1896            .duration_since(std::time::UNIX_EPOCH)
1897            .unwrap()
1898            .as_secs();
1899
1900        // Helper to create expiring nonce AA tx with given gas limit
1901        let create_expiring_nonce_tx = |gas_limit: u64| {
1902            let calls: Vec<Call> = vec![Call {
1903                to: TxKind::Call(Address::from([1u8; 20])),
1904                value: U256::ZERO,
1905                input: alloy_primitives::Bytes::from(vec![0xd0, 0x9d, 0xe0, 0x8a]), // increment()
1906            }];
1907
1908            let tx = TempoTransaction {
1909                chain_id: 1,
1910                max_priority_fee_per_gas: 1_000_000_000,
1911                max_fee_per_gas: 20_000_000_000,
1912                gas_limit,
1913                calls,
1914                nonce_key: TEMPO_EXPIRING_NONCE_KEY, // Expiring nonce
1915                nonce: 0,
1916                valid_before: Some(current_time + 25), // Valid for 25 seconds
1917                fee_token: Some(address!("0000000000000000000000000000000000000002")),
1918                ..Default::default()
1919            };
1920
1921            let signed = AASigned::new_unhashed(
1922                tx,
1923                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1924                    Signature::test_signature(),
1925                )),
1926            );
1927            TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1928        };
1929
1930        // Expiring nonce tx should only need ~35k gas (base + EXPIRING_NONCE_GAS of 13k)
1931        // NOT 250k+ which would be required for new account creation
1932        // Test: 50k gas should pass for expiring nonce (would fail if 250k was required)
1933        let tx = create_expiring_nonce_tx(50_000);
1934        let validator = setup_validator(&tx, current_time);
1935        let outcome = validator
1936            .validate_transaction(TransactionOrigin::External, tx)
1937            .await;
1938
1939        // Should NOT fail with InsufficientGasForAAIntrinsicCost or IntrinsicGasTooLow
1940        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1941            let is_intrinsic_gas_error = matches!(
1942                err.downcast_other_ref::<TempoPoolTransactionError>(),
1943                Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1944            ) || matches!(
1945                err.downcast_other_ref::<InvalidPoolTransactionError>(),
1946                Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
1947            );
1948            assert!(
1949                !is_intrinsic_gas_error,
1950                "Expiring nonce tx with 50k gas should NOT fail intrinsic gas check, got: {err:?}"
1951            );
1952        }
1953    }
1954
1955    /// Test that existing 2D nonce keys (nonce_key != 0 && nonce > 0) charge
1956    /// EXISTING_NONCE_KEY_GAS (5,000) during pool admission, matching handler.rs.
1957    ///
1958    /// Without this charge, transactions with a gas_limit 5,000 too low could
1959    /// pass pool validation but fail at execution time.
1960    #[tokio::test]
1961    async fn test_existing_2d_nonce_key_intrinsic_gas() {
1962        use alloy_primitives::{Signature, TxKind, address};
1963        use tempo_primitives::transaction::{
1964            TempoTransaction,
1965            tempo_transaction::Call,
1966            tt_signature::{PrimitiveSignature, TempoSignature},
1967            tt_signed::AASigned,
1968        };
1969
1970        let current_time = std::time::SystemTime::now()
1971            .duration_since(std::time::UNIX_EPOCH)
1972            .unwrap()
1973            .as_secs();
1974
1975        // Helper to create AA tx with a specific nonce_key and nonce
1976        let create_aa_tx = |gas_limit: u64, nonce_key: U256, nonce: u64| {
1977            let calls: Vec<Call> = vec![Call {
1978                to: TxKind::Call(Address::from([1u8; 20])),
1979                value: U256::ZERO,
1980                input: alloy_primitives::Bytes::from(vec![0xd0, 0x9d, 0xe0, 0x8a]), // increment()
1981            }];
1982
1983            let tx = TempoTransaction {
1984                chain_id: 1,
1985                max_priority_fee_per_gas: 1_000_000_000,
1986                max_fee_per_gas: 20_000_000_000,
1987                gas_limit,
1988                calls,
1989                nonce_key,
1990                nonce,
1991                fee_token: Some(address!("0000000000000000000000000000000000000002")),
1992                ..Default::default()
1993            };
1994
1995            let signed = AASigned::new_unhashed(
1996                tx,
1997                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1998                    Signature::test_signature(),
1999                )),
2000            );
2001            TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
2002        };
2003
2004        // Test 1: 1D nonce (nonce_key=0) with nonce > 0 has no extra 2D nonce charge.
2005        // 50k gas should be sufficient (base ~21k + calldata).
2006        let tx_1d = create_aa_tx(50_000, U256::ZERO, 5);
2007        let validator = setup_validator(&tx_1d, current_time);
2008        let outcome = validator
2009            .validate_transaction(TransactionOrigin::External, tx_1d)
2010            .await;
2011
2012        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2013            let is_gas_error = matches!(
2014                err.downcast_other_ref::<TempoPoolTransactionError>(),
2015                Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
2016            ) || matches!(
2017                err.downcast_other_ref::<InvalidPoolTransactionError>(),
2018                Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
2019            );
2020            assert!(
2021                !is_gas_error,
2022                "1D nonce with nonce>0 and 50k gas should NOT fail intrinsic gas check, got: {err:?}"
2023            );
2024        }
2025
2026        // Test 2: 2D nonce (nonce_key != 0) with nonce > 0, same 50k gas.
2027        // This triggers the EXISTING_NONCE_KEY_GAS branch (+5k), but 50k is still enough.
2028        let tx_2d_ok = create_aa_tx(50_000, U256::from(1), 5);
2029        let validator = setup_validator(&tx_2d_ok, current_time);
2030        let outcome = validator
2031            .validate_transaction(TransactionOrigin::External, tx_2d_ok)
2032            .await;
2033
2034        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2035            let is_gas_error = matches!(
2036                err.downcast_other_ref::<TempoPoolTransactionError>(),
2037                Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
2038            ) || matches!(
2039                err.downcast_other_ref::<InvalidPoolTransactionError>(),
2040                Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
2041            );
2042            assert!(
2043                !is_gas_error,
2044                "Existing 2D nonce key with 50k gas should NOT fail intrinsic gas check, got: {err:?}"
2045            );
2046        }
2047
2048        // Test 3: 2D nonce (nonce_key != 0), nonce > 0, with gas that is sufficient for
2049        // base intrinsic gas but NOT sufficient when EXISTING_NONCE_KEY_GAS (5k) is added.
2050        // Use 22_000 gas: enough for base ~21k + calldata but not when +5k is charged.
2051        let tx_2d_low = create_aa_tx(22_000, U256::from(1), 5);
2052        let validator = setup_validator(&tx_2d_low, current_time);
2053        let outcome = validator
2054            .validate_transaction(TransactionOrigin::External, tx_2d_low)
2055            .await;
2056
2057        match outcome {
2058            TransactionValidationOutcome::Invalid(_, ref err) => {
2059                let is_gas_error = matches!(
2060                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2061                    Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
2062                ) || matches!(
2063                    err.downcast_other_ref::<InvalidPoolTransactionError>(),
2064                    Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
2065                );
2066                assert!(
2067                    is_gas_error,
2068                    "Existing 2D nonce key with 22k gas should fail intrinsic gas check, got: {err:?}"
2069                );
2070            }
2071            _ => panic!(
2072                "Expected Invalid outcome for existing 2D nonce with insufficient gas, got: {outcome:?}"
2073            ),
2074        }
2075
2076        // Test 4: Same scenario as test 3, but with 1D nonce (nonce_key=0).
2077        // Without the 5k charge, 22k should be sufficient.
2078        let tx_1d_low = create_aa_tx(22_000, U256::ZERO, 5);
2079        let validator = setup_validator(&tx_1d_low, current_time);
2080        let outcome = validator
2081            .validate_transaction(TransactionOrigin::External, tx_1d_low)
2082            .await;
2083
2084        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2085            let is_gas_error = matches!(
2086                err.downcast_other_ref::<TempoPoolTransactionError>(),
2087                Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
2088            ) || matches!(
2089                err.downcast_other_ref::<InvalidPoolTransactionError>(),
2090                Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
2091            );
2092            assert!(
2093                !is_gas_error,
2094                "1D nonce with nonce>0 and 22k gas should NOT fail intrinsic gas check, got: {err:?}"
2095            );
2096        }
2097    }
2098
2099    #[tokio::test]
2100    async fn test_non_zero_value_in_eip1559_rejected() {
2101        let transaction = TxBuilder::eip1559(Address::random())
2102            .value(U256::from(1))
2103            .build_eip1559();
2104
2105        let current_time = std::time::SystemTime::now()
2106            .duration_since(std::time::UNIX_EPOCH)
2107            .unwrap()
2108            .as_secs();
2109        let validator = setup_validator(&transaction, current_time);
2110
2111        let outcome = validator
2112            .validate_transaction(TransactionOrigin::External, transaction)
2113            .await;
2114
2115        match outcome {
2116            TransactionValidationOutcome::Invalid(_, ref err) => {
2117                assert!(matches!(
2118                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2119                    Some(TempoPoolTransactionError::NonZeroValue)
2120                ));
2121            }
2122            _ => panic!("Expected Invalid outcome with NonZeroValue error, got: {outcome:?}"),
2123        }
2124    }
2125
2126    #[tokio::test]
2127    async fn test_zero_value_passes_value_check() {
2128        // Create a zero-value EIP-1559 transaction (value defaults to 0 in TxBuilder)
2129        let transaction = TxBuilder::eip1559(Address::random()).build_eip1559();
2130        assert!(transaction.value().is_zero(), "Test expects zero-value tx");
2131
2132        let current_time = std::time::SystemTime::now()
2133            .duration_since(std::time::UNIX_EPOCH)
2134            .unwrap()
2135            .as_secs();
2136        let validator = setup_validator(&transaction, current_time);
2137
2138        let outcome = validator
2139            .validate_transaction(TransactionOrigin::External, transaction)
2140            .await;
2141
2142        assert!(
2143            matches!(outcome, TransactionValidationOutcome::Valid { .. }),
2144            "Zero-value tx should pass validation, got: {outcome:?}"
2145        );
2146    }
2147
2148    #[tokio::test]
2149    async fn test_invalid_fee_token_rejected() {
2150        let invalid_fee_token = address!("1234567890123456789012345678901234567890");
2151
2152        let transaction = TxBuilder::aa(Address::random())
2153            .fee_token(invalid_fee_token)
2154            .build();
2155
2156        let current_time = std::time::SystemTime::now()
2157            .duration_since(std::time::UNIX_EPOCH)
2158            .unwrap()
2159            .as_secs();
2160        let validator = setup_validator(&transaction, current_time);
2161
2162        let outcome = validator
2163            .validate_transaction(TransactionOrigin::External, transaction)
2164            .await;
2165
2166        match outcome {
2167            TransactionValidationOutcome::Invalid(_, ref err) => {
2168                assert!(matches!(
2169                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2170                    Some(TempoPoolTransactionError::InvalidFeeToken(_))
2171                ));
2172            }
2173            _ => panic!("Expected Invalid outcome with InvalidFeeToken error, got: {outcome:?}"),
2174        }
2175    }
2176
2177    #[tokio::test]
2178    async fn test_aa_valid_after_and_valid_before_both_valid() {
2179        let current_time = std::time::SystemTime::now()
2180            .duration_since(std::time::UNIX_EPOCH)
2181            .unwrap()
2182            .as_secs();
2183
2184        let valid_after = current_time + 60;
2185        let valid_before = current_time + 3600;
2186
2187        let transaction = create_aa_transaction(Some(valid_after), Some(valid_before));
2188        let validator = setup_validator(&transaction, current_time);
2189
2190        let outcome = validator
2191            .validate_transaction(TransactionOrigin::External, transaction)
2192            .await;
2193
2194        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2195            let tempo_err = err.downcast_other_ref::<TempoPoolTransactionError>();
2196            assert!(
2197                !matches!(
2198                    tempo_err,
2199                    Some(TempoPoolTransactionError::InvalidValidAfter { .. })
2200                        | Some(TempoPoolTransactionError::InvalidValidBefore { .. })
2201                ),
2202                "Should not fail with validity window errors"
2203            );
2204        }
2205    }
2206
2207    #[tokio::test]
2208    async fn test_fee_cap_below_min_base_fee_rejected() {
2209        let current_time = std::time::SystemTime::now()
2210            .duration_since(std::time::UNIX_EPOCH)
2211            .unwrap()
2212            .as_secs();
2213
2214        // T0 base fee is 10 gwei (10_000_000_000 wei)
2215        // Create a transaction with max_fee_per_gas below this
2216        let transaction = TxBuilder::aa(Address::random())
2217            .max_fee(1_000_000_000) // 1 gwei, below T0's 10 gwei
2218            .max_priority_fee(1_000_000_000)
2219            .build();
2220
2221        let validator = setup_validator(&transaction, current_time);
2222
2223        let outcome = validator
2224            .validate_transaction(TransactionOrigin::External, transaction)
2225            .await;
2226
2227        match outcome {
2228            TransactionValidationOutcome::Invalid(_, ref err) => {
2229                assert!(
2230                    matches!(
2231                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2232                        Some(TempoPoolTransactionError::FeeCapBelowMinBaseFee { .. })
2233                    ),
2234                    "Expected FeeCapBelowMinBaseFee error, got: {err:?}"
2235                );
2236            }
2237            _ => panic!(
2238                "Expected Invalid outcome with FeeCapBelowMinBaseFee error, got: {outcome:?}"
2239            ),
2240        }
2241    }
2242
2243    #[tokio::test]
2244    async fn test_fee_cap_at_min_base_fee_passes() {
2245        let current_time = std::time::SystemTime::now()
2246            .duration_since(std::time::UNIX_EPOCH)
2247            .unwrap()
2248            .as_secs();
2249
2250        // Create a transaction with max_fee_per_gas exactly at minimum
2251        let active_fork = MODERATO.tempo_hardfork_at(current_time);
2252        let transaction = TxBuilder::aa(Address::random())
2253            .max_fee(active_fork.base_fee() as u128)
2254            .max_priority_fee(1_000_000_000)
2255            .build();
2256
2257        let validator = setup_validator(&transaction, current_time);
2258
2259        let outcome = validator
2260            .validate_transaction(TransactionOrigin::External, transaction)
2261            .await;
2262
2263        // Should not fail with FeeCapBelowMinBaseFee
2264        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2265            assert!(
2266                !matches!(
2267                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2268                    Some(TempoPoolTransactionError::FeeCapBelowMinBaseFee { .. })
2269                ),
2270                "Should not fail with FeeCapBelowMinBaseFee when fee cap equals min base fee"
2271            );
2272        }
2273    }
2274
2275    #[tokio::test]
2276    async fn test_fee_cap_above_min_base_fee_passes() {
2277        let current_time = std::time::SystemTime::now()
2278            .duration_since(std::time::UNIX_EPOCH)
2279            .unwrap()
2280            .as_secs();
2281
2282        // T0 base fee is 10 gwei (10_000_000_000 wei)
2283        // Create a transaction with max_fee_per_gas above minimum
2284        let transaction = TxBuilder::aa(Address::random())
2285            .max_fee(20_000_000_000) // 20 gwei, above T0's 10 gwei
2286            .max_priority_fee(1_000_000_000)
2287            .build();
2288
2289        let validator = setup_validator(&transaction, current_time);
2290
2291        let outcome = validator
2292            .validate_transaction(TransactionOrigin::External, transaction)
2293            .await;
2294
2295        // Should not fail with FeeCapBelowMinBaseFee
2296        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2297            assert!(
2298                !matches!(
2299                    err.downcast_other_ref::<TempoPoolTransactionError>(),
2300                    Some(TempoPoolTransactionError::FeeCapBelowMinBaseFee { .. })
2301                ),
2302                "Should not fail with FeeCapBelowMinBaseFee when fee cap is above min base fee"
2303            );
2304        }
2305    }
2306
2307    #[tokio::test]
2308    async fn test_eip1559_fee_cap_below_min_base_fee_rejected() {
2309        let current_time = std::time::SystemTime::now()
2310            .duration_since(std::time::UNIX_EPOCH)
2311            .unwrap()
2312            .as_secs();
2313
2314        // T0 base fee is 10 gwei, create EIP-1559 tx with lower fee
2315        let transaction = TxBuilder::eip1559(Address::random())
2316            .max_fee(1_000_000_000) // 1 gwei, below T0's 10 gwei
2317            .max_priority_fee(1_000_000_000)
2318            .build_eip1559();
2319
2320        let validator = setup_validator(&transaction, current_time);
2321
2322        let outcome = validator
2323            .validate_transaction(TransactionOrigin::External, transaction)
2324            .await;
2325
2326        match outcome {
2327            TransactionValidationOutcome::Invalid(_, ref err) => {
2328                assert!(
2329                    matches!(
2330                        err.downcast_other_ref::<TempoPoolTransactionError>(),
2331                        Some(TempoPoolTransactionError::FeeCapBelowMinBaseFee { .. })
2332                    ),
2333                    "Expected FeeCapBelowMinBaseFee error for EIP-1559 tx, got: {err:?}"
2334                );
2335            }
2336            _ => panic!(
2337                "Expected Invalid outcome with FeeCapBelowMinBaseFee error, got: {outcome:?}"
2338            ),
2339        }
2340    }
2341
2342    mod keychain_validation {
2343        use super::*;
2344        use alloy_primitives::{Signature, TxKind, address};
2345        use alloy_signer::SignerSync;
2346        use alloy_signer_local::PrivateKeySigner;
2347        use reth_chainspec::ForkCondition;
2348        use reth_transaction_pool::error::PoolTransactionError;
2349        use tempo_chainspec::hardfork::TempoHardfork;
2350        use tempo_primitives::transaction::{
2351            KeyAuthorization, SignatureType, SignedKeyAuthorization, TempoTransaction, TokenLimit,
2352            tempo_transaction::Call,
2353            tt_signature::{
2354                KeychainSignature, KeychainVersion, PrimitiveSignature, TempoSignature,
2355            },
2356            tt_signed::AASigned,
2357        };
2358
2359        /// Returns a MODERATO chain spec with T1C activated at timestamp 0.
2360        fn moderato_with_t1c() -> TempoChainSpec {
2361            let mut spec = Arc::unwrap_or_clone(MODERATO.clone());
2362            spec.inner
2363                .hardforks
2364                .extend([(TempoHardfork::T1C, ForkCondition::Timestamp(0))]);
2365            spec
2366        }
2367
2368        /// Generate a secp256k1 keypair for testing
2369        fn generate_keypair() -> (PrivateKeySigner, Address) {
2370            let signer = PrivateKeySigner::random();
2371            let address = signer.address();
2372            (signer, address)
2373        }
2374
2375        /// Create an AA transaction with a V2 keychain signature.
2376        fn create_aa_with_keychain_signature(
2377            user_address: Address,
2378            access_key_signer: &PrivateKeySigner,
2379            key_authorization: Option<SignedKeyAuthorization>,
2380        ) -> TempoPooledTransaction {
2381            create_aa_with_keychain_signature_versioned(
2382                user_address,
2383                access_key_signer,
2384                key_authorization,
2385                KeychainVersion::V2,
2386            )
2387        }
2388
2389        /// Create an AA transaction with a V1 (legacy) keychain signature.
2390        fn create_aa_with_v1_keychain_signature(
2391            user_address: Address,
2392            access_key_signer: &PrivateKeySigner,
2393            key_authorization: Option<SignedKeyAuthorization>,
2394        ) -> TempoPooledTransaction {
2395            create_aa_with_keychain_signature_versioned(
2396                user_address,
2397                access_key_signer,
2398                key_authorization,
2399                KeychainVersion::V1,
2400            )
2401        }
2402
2403        /// Create an AA transaction with a keychain signature of the specified version.
2404        fn create_aa_with_keychain_signature_versioned(
2405            user_address: Address,
2406            access_key_signer: &PrivateKeySigner,
2407            key_authorization: Option<SignedKeyAuthorization>,
2408            version: KeychainVersion,
2409        ) -> TempoPooledTransaction {
2410            let tx_aa = TempoTransaction {
2411                chain_id: 42431, // MODERATO chain_id
2412                max_priority_fee_per_gas: 1_000_000_000,
2413                max_fee_per_gas: 20_000_000_000,
2414                gas_limit: 1_000_000,
2415                calls: vec![Call {
2416                    to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
2417                    value: U256::ZERO,
2418                    input: alloy_primitives::Bytes::new(),
2419                }],
2420                nonce_key: U256::ZERO,
2421                nonce: 0,
2422                fee_token: Some(address!("0000000000000000000000000000000000000002")),
2423                fee_payer_signature: None,
2424                valid_after: None,
2425                valid_before: None,
2426                access_list: Default::default(),
2427                tempo_authorization_list: vec![],
2428                key_authorization,
2429            };
2430
2431            // Create unsigned transaction to get the signature hash
2432            let unsigned = AASigned::new_unhashed(
2433                tx_aa.clone(),
2434                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2435                    Signature::test_signature(),
2436                )),
2437            );
2438            let sig_hash = unsigned.signature_hash();
2439
2440            let keychain_sig = match version {
2441                KeychainVersion::V1 => {
2442                    let signature = access_key_signer
2443                        .sign_hash_sync(&sig_hash)
2444                        .expect("signing failed");
2445                    TempoSignature::Keychain(KeychainSignature::new_v1(
2446                        user_address,
2447                        PrimitiveSignature::Secp256k1(signature),
2448                    ))
2449                }
2450                KeychainVersion::V2 => {
2451                    let sig_hash = KeychainSignature::signing_hash(sig_hash, user_address);
2452                    let signature = access_key_signer
2453                        .sign_hash_sync(&sig_hash)
2454                        .expect("signing failed");
2455                    TempoSignature::Keychain(KeychainSignature::new(
2456                        user_address,
2457                        PrimitiveSignature::Secp256k1(signature),
2458                    ))
2459                }
2460            };
2461
2462            let signed_tx = AASigned::new_unhashed(tx_aa, keychain_sig);
2463            let envelope: TempoTxEnvelope = signed_tx.into();
2464            let recovered = envelope.try_into_recovered().unwrap();
2465            TempoPooledTransaction::new(recovered)
2466        }
2467
2468        fn validate_against_keychain_default_fee_context(
2469            validator: &TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>>,
2470            transaction: &TempoPooledTransaction,
2471            state_provider: &mut impl StateProvider,
2472        ) -> Result<Result<(), TempoPoolTransactionError>, ProviderError> {
2473            validator.validate_against_keychain(
2474                transaction,
2475                state_provider,
2476                transaction.sender(),
2477                transaction
2478                    .inner()
2479                    .fee_token()
2480                    .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN),
2481            )
2482        }
2483
2484        /// Setup validator with keychain storage for a specific user and key_id.
2485        fn setup_validator_with_keychain_storage(
2486            transaction: &TempoPooledTransaction,
2487            user_address: Address,
2488            key_id: Address,
2489            authorized_key_slot_value: Option<U256>,
2490        ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
2491            setup_validator_with_keychain_storage_spec(
2492                transaction,
2493                user_address,
2494                key_id,
2495                authorized_key_slot_value,
2496                moderato_with_t1c(),
2497            )
2498        }
2499
2500        fn setup_validator_with_keychain_storage_spec(
2501            transaction: &TempoPooledTransaction,
2502            user_address: Address,
2503            key_id: Address,
2504            authorized_key_slot_value: Option<U256>,
2505            chain_spec: TempoChainSpec,
2506        ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
2507            let provider = MockEthProvider::<TempoPrimitives>::new().with_chain_spec(chain_spec);
2508
2509            // Add sender account
2510            provider.add_account(
2511                transaction.sender(),
2512                ExtendedAccount::new(transaction.nonce(), U256::ZERO),
2513            );
2514            provider.add_block(B256::random(), Default::default());
2515
2516            // If slot value provided, setup AccountKeychain storage
2517            if let Some(slot_value) = authorized_key_slot_value {
2518                let storage_slot = AccountKeychain::new().keys[user_address][key_id].base_slot();
2519                provider.add_account(
2520                    ACCOUNT_KEYCHAIN_ADDRESS,
2521                    ExtendedAccount::new(0, U256::ZERO)
2522                        .extend_storage([(storage_slot.into(), slot_value)]),
2523                );
2524            }
2525
2526            let inner =
2527                EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
2528                    .disable_balance_check()
2529                    .build(InMemoryBlobStore::default());
2530            let amm_cache =
2531                AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
2532            TempoTransactionValidator::new(
2533                inner,
2534                DEFAULT_AA_VALID_AFTER_MAX_SECS,
2535                DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
2536                amm_cache,
2537            )
2538        }
2539
2540        #[test]
2541        fn test_non_aa_transaction_skips_keychain_validation() -> Result<(), ProviderError> {
2542            // Non-AA transaction should return Ok(Ok(())) immediately
2543            let transaction = TxBuilder::eip1559(Address::random()).build_eip1559();
2544            let validator = setup_validator(&transaction, 0);
2545            let mut state_provider = validator.inner.client().latest().unwrap();
2546
2547            let result = validate_against_keychain_default_fee_context(
2548                &validator,
2549                &transaction,
2550                &mut state_provider,
2551            )?;
2552            assert!(result.is_ok(), "Non-AA tx should skip keychain validation");
2553            Ok(())
2554        }
2555
2556        #[test]
2557        fn test_aa_with_primitive_signature_skips_keychain_validation() -> Result<(), ProviderError>
2558        {
2559            // AA transaction with primitive (non-keychain) signature should skip validation
2560            let transaction = create_aa_transaction(None, None);
2561            let validator = setup_validator(&transaction, 0);
2562            let mut state_provider = validator.inner.client().latest().unwrap();
2563
2564            let result = validate_against_keychain_default_fee_context(
2565                &validator,
2566                &transaction,
2567                &mut state_provider,
2568            )?;
2569            assert!(
2570                result.is_ok(),
2571                "AA tx with primitive signature should skip keychain validation"
2572            );
2573            Ok(())
2574        }
2575
2576        #[test]
2577        fn test_keychain_signature_with_valid_authorized_key() -> Result<(), ProviderError> {
2578            let (access_key_signer, access_key_address) = generate_keypair();
2579            let user_address = Address::random();
2580
2581            let transaction =
2582                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
2583
2584            // Setup storage with a valid authorized key (expiry > 0, not revoked)
2585            let slot_value = AuthorizedKey {
2586                signature_type: 0, // secp256k1
2587                expiry: u64::MAX,  // never expires
2588                enforce_limits: false,
2589                is_revoked: false,
2590            }
2591            .encode_to_slot();
2592
2593            let validator = setup_validator_with_keychain_storage(
2594                &transaction,
2595                user_address,
2596                access_key_address,
2597                Some(slot_value),
2598            );
2599            let mut state_provider = validator.inner.client().latest().unwrap();
2600
2601            let result = validate_against_keychain_default_fee_context(
2602                &validator,
2603                &transaction,
2604                &mut state_provider,
2605            )?;
2606            assert!(
2607                result.is_ok(),
2608                "Valid authorized key should pass validation, got: {result:?}"
2609            );
2610            Ok(())
2611        }
2612
2613        #[test]
2614        fn test_keychain_signature_with_revoked_key_rejected() {
2615            let (access_key_signer, access_key_address) = generate_keypair();
2616            let user_address = Address::random();
2617
2618            let transaction =
2619                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
2620
2621            // Setup storage with a revoked key
2622            let slot_value = AuthorizedKey {
2623                signature_type: 0,
2624                expiry: 0, // revoked keys have expiry=0
2625                enforce_limits: false,
2626                is_revoked: true,
2627            }
2628            .encode_to_slot();
2629
2630            let validator = setup_validator_with_keychain_storage(
2631                &transaction,
2632                user_address,
2633                access_key_address,
2634                Some(slot_value),
2635            );
2636            let mut state_provider = validator.inner.client().latest().unwrap();
2637
2638            let result = validate_against_keychain_default_fee_context(
2639                &validator,
2640                &transaction,
2641                &mut state_provider,
2642            );
2643            assert!(
2644                matches!(
2645                    result.expect("should not be a provider error"),
2646                    Err(TempoPoolTransactionError::Keychain(
2647                        "access key has been revoked"
2648                    ))
2649                ),
2650                "Revoked key should be rejected"
2651            );
2652        }
2653
2654        #[test]
2655        fn test_keychain_signature_with_nonexistent_key_rejected() {
2656            let (access_key_signer, access_key_address) = generate_keypair();
2657            let user_address = Address::random();
2658
2659            let transaction =
2660                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
2661
2662            // Setup storage with expiry = 0 (key doesn't exist)
2663            let slot_value = AuthorizedKey {
2664                signature_type: 0,
2665                expiry: 0, // expiry = 0 means key doesn't exist
2666                enforce_limits: false,
2667                is_revoked: false,
2668            }
2669            .encode_to_slot();
2670
2671            let validator = setup_validator_with_keychain_storage(
2672                &transaction,
2673                user_address,
2674                access_key_address,
2675                Some(slot_value),
2676            );
2677            let mut state_provider = validator.inner.client().latest().unwrap();
2678
2679            let result = validate_against_keychain_default_fee_context(
2680                &validator,
2681                &transaction,
2682                &mut state_provider,
2683            );
2684            assert!(
2685                matches!(
2686                    result.expect("should not be a provider error"),
2687                    Err(TempoPoolTransactionError::Keychain(
2688                        "access key does not exist"
2689                    ))
2690                ),
2691                "Non-existent key should be rejected"
2692            );
2693        }
2694
2695        #[test]
2696        fn test_keychain_signature_with_no_storage_rejected() {
2697            let (access_key_signer, _) = generate_keypair();
2698            let user_address = Address::random();
2699
2700            let transaction =
2701                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
2702
2703            // No storage setup - slot value defaults to 0
2704            let validator = setup_validator_with_keychain_storage(
2705                &transaction,
2706                user_address,
2707                Address::ZERO,
2708                None,
2709            );
2710            let mut state_provider = validator.inner.client().latest().unwrap();
2711
2712            let result = validate_against_keychain_default_fee_context(
2713                &validator,
2714                &transaction,
2715                &mut state_provider,
2716            );
2717            assert!(
2718                matches!(
2719                    result.expect("should not be a provider error"),
2720                    Err(TempoPoolTransactionError::Keychain(
2721                        "access key does not exist"
2722                    ))
2723                ),
2724                "Missing storage should result in non-existent key error"
2725            );
2726        }
2727
2728        #[test]
2729        fn test_key_authorization_without_existing_key_passes() -> Result<(), ProviderError> {
2730            let (access_key_signer, access_key_address) = generate_keypair();
2731            let (user_signer, user_address) = generate_keypair();
2732
2733            // Create KeyAuthorization signed by the user's main key
2734            let key_auth = KeyAuthorization {
2735                chain_id: 42431, // MODERATO chain_id
2736                key_type: SignatureType::Secp256k1,
2737                key_id: access_key_address,
2738                expiry: None, // never expires
2739                limits: None, // unlimited
2740            };
2741
2742            let auth_sig_hash = key_auth.signature_hash();
2743            let auth_signature = user_signer
2744                .sign_hash_sync(&auth_sig_hash)
2745                .expect("signing failed");
2746            let signed_key_auth =
2747                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2748
2749            let transaction = create_aa_with_keychain_signature(
2750                user_address,
2751                &access_key_signer,
2752                Some(signed_key_auth),
2753            );
2754
2755            // No key exists yet, so same-tx key authorization should pass.
2756            let validator = setup_validator_with_keychain_storage(
2757                &transaction,
2758                user_address,
2759                access_key_address,
2760                None,
2761            );
2762            let mut state_provider = validator.inner.client().latest().unwrap();
2763
2764            let result = validate_against_keychain_default_fee_context(
2765                &validator,
2766                &transaction,
2767                &mut state_provider,
2768            )?;
2769            assert!(
2770                result.is_ok(),
2771                "Valid KeyAuthorization should pass when key does not exist, got: {result:?}"
2772            );
2773            Ok(())
2774        }
2775
2776        #[test]
2777        fn test_key_authorization_with_existing_key_rejected() {
2778            let (access_key_signer, access_key_address) = generate_keypair();
2779            let (user_signer, user_address) = generate_keypair();
2780
2781            let key_auth = KeyAuthorization {
2782                chain_id: 42431,
2783                key_type: SignatureType::Secp256k1,
2784                key_id: access_key_address,
2785                expiry: None,
2786                limits: None,
2787            };
2788
2789            let auth_sig_hash = key_auth.signature_hash();
2790            let auth_signature = user_signer
2791                .sign_hash_sync(&auth_sig_hash)
2792                .expect("signing failed");
2793            let signed_key_auth =
2794                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2795
2796            let transaction = create_aa_with_keychain_signature(
2797                user_address,
2798                &access_key_signer,
2799                Some(signed_key_auth),
2800            );
2801
2802            let existing_key_slot = AuthorizedKey {
2803                signature_type: 0,
2804                expiry: u64::MAX,
2805                enforce_limits: false,
2806                is_revoked: false,
2807            }
2808            .encode_to_slot();
2809
2810            let validator = setup_validator_with_keychain_storage(
2811                &transaction,
2812                user_address,
2813                access_key_address,
2814                Some(existing_key_slot),
2815            );
2816            let mut state_provider = validator.inner.client().latest().unwrap();
2817
2818            let result = validate_against_keychain_default_fee_context(
2819                &validator,
2820                &transaction,
2821                &mut state_provider,
2822            );
2823            assert!(
2824                matches!(
2825                    result.expect("should not be a provider error"),
2826                    Err(TempoPoolTransactionError::Keychain(
2827                        "access key already exists"
2828                    ))
2829                ),
2830                "KeyAuthorization should be rejected when key already exists"
2831            );
2832        }
2833
2834        #[test]
2835        fn test_key_authorization_spending_limit_exceeded_rejected() {
2836            let (access_key_signer, access_key_address) = generate_keypair();
2837            let (user_signer, user_address) = generate_keypair();
2838            let fee_token = address!("0000000000000000000000000000000000000002");
2839
2840            let key_auth = KeyAuthorization {
2841                chain_id: 42431,
2842                key_type: SignatureType::Secp256k1,
2843                key_id: access_key_address,
2844                expiry: None,
2845                limits: Some(vec![TokenLimit {
2846                    token: fee_token,
2847                    limit: U256::ZERO,
2848                }]),
2849            };
2850
2851            let auth_sig_hash = key_auth.signature_hash();
2852            let auth_signature = user_signer
2853                .sign_hash_sync(&auth_sig_hash)
2854                .expect("signing failed");
2855            let signed_key_auth =
2856                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2857
2858            let transaction = create_aa_with_keychain_signature(
2859                user_address,
2860                &access_key_signer,
2861                Some(signed_key_auth),
2862            );
2863
2864            let validator = setup_validator_with_keychain_storage(
2865                &transaction,
2866                user_address,
2867                access_key_address,
2868                None,
2869            );
2870            let mut state_provider = validator.inner.client().latest().unwrap();
2871
2872            let result = validate_against_keychain_default_fee_context(
2873                &validator,
2874                &transaction,
2875                &mut state_provider,
2876            );
2877            assert!(
2878                matches!(
2879                    result.expect("should not be a provider error"),
2880                    Err(TempoPoolTransactionError::SpendingLimitExceeded {
2881                        fee_token: rejected_fee_token,
2882                        remaining,
2883                        ..
2884                    }) if rejected_fee_token == fee_token && remaining == U256::ZERO
2885                ),
2886                "KeyAuthorization with insufficient fee-token limit should be rejected"
2887            );
2888        }
2889
2890        #[test]
2891        fn test_key_authorization_empty_limits_rejected() {
2892            let (access_key_signer, access_key_address) = generate_keypair();
2893            let (user_signer, user_address) = generate_keypair();
2894            let fee_token = address!("0000000000000000000000000000000000000002");
2895
2896            let key_auth = KeyAuthorization {
2897                chain_id: 42431,
2898                key_type: SignatureType::Secp256k1,
2899                key_id: access_key_address,
2900                expiry: None,
2901                limits: Some(vec![]),
2902            };
2903
2904            let auth_sig_hash = key_auth.signature_hash();
2905            let auth_signature = user_signer
2906                .sign_hash_sync(&auth_sig_hash)
2907                .expect("signing failed");
2908            let signed_key_auth =
2909                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2910
2911            let transaction = create_aa_with_keychain_signature(
2912                user_address,
2913                &access_key_signer,
2914                Some(signed_key_auth),
2915            );
2916
2917            let validator = setup_validator_with_keychain_storage(
2918                &transaction,
2919                user_address,
2920                access_key_address,
2921                None,
2922            );
2923            let mut state_provider = validator.inner.client().latest().unwrap();
2924
2925            let result = validate_against_keychain_default_fee_context(
2926                &validator,
2927                &transaction,
2928                &mut state_provider,
2929            );
2930            assert!(
2931                matches!(
2932                    result.expect("should not be a provider error"),
2933                    Err(TempoPoolTransactionError::SpendingLimitExceeded {
2934                        fee_token: rejected_fee_token,
2935                        remaining,
2936                        ..
2937                    }) if rejected_fee_token == fee_token && remaining == U256::ZERO
2938                ),
2939                "KeyAuthorization with empty limits should be rejected"
2940            );
2941        }
2942
2943        #[test]
2944        fn test_key_authorization_fee_token_not_in_limits_rejected() {
2945            let (access_key_signer, access_key_address) = generate_keypair();
2946            let (user_signer, user_address) = generate_keypair();
2947            let fee_token = address!("0000000000000000000000000000000000000002");
2948            let non_fee_token = Address::random();
2949            assert_ne!(non_fee_token, fee_token);
2950
2951            let key_auth = KeyAuthorization {
2952                chain_id: 42431,
2953                key_type: SignatureType::Secp256k1,
2954                key_id: access_key_address,
2955                expiry: None,
2956                limits: Some(vec![TokenLimit {
2957                    token: non_fee_token,
2958                    limit: U256::MAX,
2959                }]),
2960            };
2961
2962            let auth_sig_hash = key_auth.signature_hash();
2963            let auth_signature = user_signer
2964                .sign_hash_sync(&auth_sig_hash)
2965                .expect("signing failed");
2966            let signed_key_auth =
2967                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2968
2969            let transaction = create_aa_with_keychain_signature(
2970                user_address,
2971                &access_key_signer,
2972                Some(signed_key_auth),
2973            );
2974
2975            let validator = setup_validator_with_keychain_storage(
2976                &transaction,
2977                user_address,
2978                access_key_address,
2979                None,
2980            );
2981            let mut state_provider = validator.inner.client().latest().unwrap();
2982
2983            let result = validate_against_keychain_default_fee_context(
2984                &validator,
2985                &transaction,
2986                &mut state_provider,
2987            );
2988            assert!(
2989                matches!(
2990                    result.expect("should not be a provider error"),
2991                    Err(TempoPoolTransactionError::SpendingLimitExceeded {
2992                        fee_token: rejected_fee_token,
2993                        remaining,
2994                        ..
2995                    }) if rejected_fee_token == fee_token && remaining == U256::ZERO
2996                ),
2997                "KeyAuthorization should reject when limits omit the fee token"
2998            );
2999        }
3000
3001        #[test]
3002        fn test_key_authorization_duplicate_token_limits_uses_last_value()
3003        -> Result<(), ProviderError> {
3004            let (access_key_signer, access_key_address) = generate_keypair();
3005            let (user_signer, user_address) = generate_keypair();
3006            let fee_token = address!("0000000000000000000000000000000000000002");
3007
3008            let probe_tx =
3009                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
3010            let fee_cost = probe_tx.fee_token_cost();
3011
3012            // Duplicate limits for the same token: execution keeps the last write.
3013            let key_auth = KeyAuthorization {
3014                chain_id: 42431,
3015                key_type: SignatureType::Secp256k1,
3016                key_id: access_key_address,
3017                expiry: None,
3018                limits: Some(vec![
3019                    TokenLimit {
3020                        token: fee_token,
3021                        limit: U256::ZERO,
3022                    },
3023                    TokenLimit {
3024                        token: fee_token,
3025                        limit: fee_cost + U256::from(100),
3026                    },
3027                ]),
3028            };
3029
3030            let auth_sig_hash = key_auth.signature_hash();
3031            let auth_signature = user_signer
3032                .sign_hash_sync(&auth_sig_hash)
3033                .expect("signing failed");
3034            let signed_key_auth =
3035                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3036
3037            let transaction = create_aa_with_keychain_signature(
3038                user_address,
3039                &access_key_signer,
3040                Some(signed_key_auth),
3041            );
3042
3043            let validator = setup_validator_with_keychain_storage(
3044                &transaction,
3045                user_address,
3046                access_key_address,
3047                None,
3048            );
3049            let mut state_provider = validator.inner.client().latest().unwrap();
3050
3051            let result = validate_against_keychain_default_fee_context(
3052                &validator,
3053                &transaction,
3054                &mut state_provider,
3055            )?;
3056            assert!(
3057                result.is_ok(),
3058                "Inline key authorization should use the last duplicate token limit"
3059            );
3060            Ok(())
3061        }
3062
3063        #[test]
3064        fn test_key_authorization_spending_limit_uses_resolved_fee_token()
3065        -> Result<(), ProviderError> {
3066            let (access_key_signer, access_key_address) = generate_keypair();
3067            let (user_signer, user_address) = generate_keypair();
3068            let resolved_fee_token = Address::random();
3069
3070            let key_auth = KeyAuthorization {
3071                chain_id: 42431,
3072                key_type: SignatureType::Secp256k1,
3073                key_id: access_key_address,
3074                expiry: None,
3075                limits: Some(vec![TokenLimit {
3076                    token: resolved_fee_token,
3077                    limit: U256::MAX,
3078                }]),
3079            };
3080
3081            let auth_sig_hash = key_auth.signature_hash();
3082            let auth_signature = user_signer
3083                .sign_hash_sync(&auth_sig_hash)
3084                .expect("signing failed");
3085            let signed_key_auth =
3086                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3087
3088            let transaction = create_aa_with_keychain_signature(
3089                user_address,
3090                &access_key_signer,
3091                Some(signed_key_auth),
3092            );
3093
3094            let validator = setup_validator_with_keychain_storage(
3095                &transaction,
3096                user_address,
3097                access_key_address,
3098                None,
3099            );
3100            let mut state_provider = validator.inner.client().latest().unwrap();
3101
3102            let result = validator.validate_against_keychain(
3103                &transaction,
3104                &mut state_provider,
3105                user_address,
3106                resolved_fee_token,
3107            )?;
3108            assert!(
3109                result.is_ok(),
3110                "Inline key authorization should use the resolved fee token"
3111            );
3112            Ok(())
3113        }
3114
3115        #[test]
3116        fn test_key_authorization_spending_limit_skipped_for_sponsored_fee_payer()
3117        -> Result<(), ProviderError> {
3118            let (access_key_signer, access_key_address) = generate_keypair();
3119            let (user_signer, user_address) = generate_keypair();
3120            let fee_token = address!("0000000000000000000000000000000000000002");
3121
3122            let key_auth = KeyAuthorization {
3123                chain_id: 42431,
3124                key_type: SignatureType::Secp256k1,
3125                key_id: access_key_address,
3126                expiry: None,
3127                limits: Some(vec![TokenLimit {
3128                    token: fee_token,
3129                    limit: U256::ZERO,
3130                }]),
3131            };
3132
3133            let auth_sig_hash = key_auth.signature_hash();
3134            let auth_signature = user_signer
3135                .sign_hash_sync(&auth_sig_hash)
3136                .expect("signing failed");
3137            let signed_key_auth =
3138                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3139
3140            let transaction = create_aa_with_keychain_signature(
3141                user_address,
3142                &access_key_signer,
3143                Some(signed_key_auth),
3144            );
3145
3146            let validator = setup_validator_with_keychain_storage(
3147                &transaction,
3148                user_address,
3149                access_key_address,
3150                None,
3151            );
3152            let mut state_provider = validator.inner.client().latest().unwrap();
3153
3154            let sponsored_fee_payer = Address::random();
3155            assert_ne!(sponsored_fee_payer, user_address);
3156
3157            let result = validator.validate_against_keychain(
3158                &transaction,
3159                &mut state_provider,
3160                sponsored_fee_payer,
3161                fee_token,
3162            )?;
3163            assert!(
3164                result.is_ok(),
3165                "Inline key authorization spending limits should be skipped for sponsored transactions"
3166            );
3167            Ok(())
3168        }
3169
3170        /// Setup a validator using the DEV chain spec (T1C active at genesis).
3171        fn setup_validator_with_keychain_storage_t1c(
3172            transaction: &TempoPooledTransaction,
3173            user_address: Address,
3174            key_id: Address,
3175            authorized_key_slot_value: Option<U256>,
3176        ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
3177            use tempo_chainspec::spec::DEV;
3178
3179            setup_validator_with_keychain_storage_spec(
3180                transaction,
3181                user_address,
3182                key_id,
3183                authorized_key_slot_value,
3184                Arc::unwrap_or_clone(DEV.clone()),
3185            )
3186        }
3187
3188        /// Helper: sign a KeyAuthorization and build a V2 transaction with it.
3189        fn build_key_auth_tx(
3190            chain_id: u64,
3191            access_key_signer: &PrivateKeySigner,
3192            access_key_address: Address,
3193            user_signer: &PrivateKeySigner,
3194            user_address: Address,
3195        ) -> TempoPooledTransaction {
3196            build_key_auth_tx_versioned(
3197                chain_id,
3198                access_key_signer,
3199                access_key_address,
3200                user_signer,
3201                user_address,
3202                KeychainVersion::V2,
3203            )
3204        }
3205
3206        /// Helper: sign a KeyAuthorization and build a transaction with the given
3207        /// keychain version.
3208        fn build_key_auth_tx_versioned(
3209            chain_id: u64,
3210            access_key_signer: &PrivateKeySigner,
3211            access_key_address: Address,
3212            user_signer: &PrivateKeySigner,
3213            user_address: Address,
3214            version: KeychainVersion,
3215        ) -> TempoPooledTransaction {
3216            let key_auth = KeyAuthorization {
3217                chain_id,
3218                key_type: SignatureType::Secp256k1,
3219                key_id: access_key_address,
3220                expiry: None,
3221                limits: None,
3222            };
3223            let auth_sig_hash = key_auth.signature_hash();
3224            let auth_signature = user_signer
3225                .sign_hash_sync(&auth_sig_hash)
3226                .expect("signing failed");
3227            let signed_key_auth =
3228                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3229            create_aa_with_keychain_signature_versioned(
3230                user_address,
3231                access_key_signer,
3232                Some(signed_key_auth),
3233                version,
3234            )
3235        }
3236
3237        /// Pre-T1C (MODERATO): chain_id=0 wildcard is accepted, wrong chain_id is rejected,
3238        /// matching chain_id is accepted.
3239        #[test]
3240        fn test_key_authorization_chain_id_pre_t1c() -> Result<(), ProviderError> {
3241            let (access_key_signer, access_key_address) = generate_keypair();
3242            let (user_signer, user_address) = generate_keypair();
3243            let moderato = Arc::unwrap_or_clone(MODERATO.clone());
3244
3245            // chain_id=0 (wildcard) → accepted
3246            let tx = build_key_auth_tx_versioned(
3247                0,
3248                &access_key_signer,
3249                access_key_address,
3250                &user_signer,
3251                user_address,
3252                KeychainVersion::V1,
3253            );
3254            let validator = setup_validator_with_keychain_storage_spec(
3255                &tx,
3256                user_address,
3257                access_key_address,
3258                None,
3259                moderato.clone(),
3260            );
3261            let mut sp = validator.inner.client().latest().unwrap();
3262            let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp)?;
3263            assert!(
3264                result.is_ok(),
3265                "chain_id=0 should be accepted pre-T1C, got: {result:?}"
3266            );
3267
3268            // chain_id=42431 (matching MODERATO) → accepted
3269            let tx = build_key_auth_tx_versioned(
3270                42431,
3271                &access_key_signer,
3272                access_key_address,
3273                &user_signer,
3274                user_address,
3275                KeychainVersion::V1,
3276            );
3277            let validator = setup_validator_with_keychain_storage_spec(
3278                &tx,
3279                user_address,
3280                access_key_address,
3281                None,
3282                moderato.clone(),
3283            );
3284            let mut sp = validator.inner.client().latest().unwrap();
3285            let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp)?;
3286            assert!(
3287                result.is_ok(),
3288                "matching chain_id should be accepted pre-T1C, got: {result:?}"
3289            );
3290
3291            // chain_id=99999 (wrong) → rejected
3292            let tx = build_key_auth_tx_versioned(
3293                99999,
3294                &access_key_signer,
3295                access_key_address,
3296                &user_signer,
3297                user_address,
3298                KeychainVersion::V1,
3299            );
3300            let validator = setup_validator_with_keychain_storage_spec(
3301                &tx,
3302                user_address,
3303                access_key_address,
3304                None,
3305                moderato,
3306            );
3307            let mut sp = validator.inner.client().latest().unwrap();
3308            let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp);
3309            assert!(
3310                matches!(
3311                    result.expect("should not be a provider error"),
3312                    Err(TempoPoolTransactionError::Keychain(
3313                        "KeyAuthorization chain_id does not match current chain"
3314                    ))
3315                ),
3316                "wrong chain_id should be rejected pre-T1C"
3317            );
3318
3319            Ok(())
3320        }
3321
3322        /// Post-T1C (DEV): chain_id=0 wildcard is rejected, wrong chain_id is rejected,
3323        /// matching chain_id is accepted.
3324        #[test]
3325        fn test_key_authorization_chain_id_post_t1c() -> Result<(), ProviderError> {
3326            use tempo_chainspec::spec::DEV;
3327
3328            let (access_key_signer, access_key_address) = generate_keypair();
3329            let (user_signer, user_address) = generate_keypair();
3330
3331            // chain_id=DEV.chain_id() (1337, matching) → accepted
3332            let tx = build_key_auth_tx(
3333                DEV.chain_id(),
3334                &access_key_signer,
3335                access_key_address,
3336                &user_signer,
3337                user_address,
3338            );
3339            let validator = setup_validator_with_keychain_storage_t1c(
3340                &tx,
3341                user_address,
3342                access_key_address,
3343                None,
3344            );
3345            let mut sp = validator.inner.client().latest().unwrap();
3346            let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp)?;
3347            assert!(
3348                result.is_ok(),
3349                "matching chain_id should be accepted post-T1C, got: {result:?}"
3350            );
3351
3352            // chain_id=0 (wildcard) → rejected
3353            let tx = build_key_auth_tx(
3354                0,
3355                &access_key_signer,
3356                access_key_address,
3357                &user_signer,
3358                user_address,
3359            );
3360            let validator = setup_validator_with_keychain_storage_t1c(
3361                &tx,
3362                user_address,
3363                access_key_address,
3364                None,
3365            );
3366            let mut sp = validator.inner.client().latest().unwrap();
3367            let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp);
3368            assert!(
3369                matches!(
3370                    result.expect("should not be a provider error"),
3371                    Err(TempoPoolTransactionError::Keychain(
3372                        "KeyAuthorization chain_id does not match current chain"
3373                    ))
3374                ),
3375                "chain_id=0 wildcard should be rejected post-T1C"
3376            );
3377
3378            // chain_id=99999 (wrong) → rejected
3379            let tx = build_key_auth_tx(
3380                99999,
3381                &access_key_signer,
3382                access_key_address,
3383                &user_signer,
3384                user_address,
3385            );
3386            let validator = setup_validator_with_keychain_storage_t1c(
3387                &tx,
3388                user_address,
3389                access_key_address,
3390                None,
3391            );
3392            let mut sp = validator.inner.client().latest().unwrap();
3393            let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp);
3394            assert!(
3395                matches!(
3396                    result.expect("should not be a provider error"),
3397                    Err(TempoPoolTransactionError::Keychain(
3398                        "KeyAuthorization chain_id does not match current chain"
3399                    ))
3400                ),
3401                "wrong chain_id should be rejected post-T1C"
3402            );
3403
3404            Ok(())
3405        }
3406
3407        #[test]
3408        fn test_key_authorization_mismatched_key_id_rejected() {
3409            let (access_key_signer, _access_key_address) = generate_keypair();
3410            let (user_signer, user_address) = generate_keypair();
3411            let different_key_id = Address::random();
3412
3413            // Create KeyAuthorization with a DIFFERENT key_id than the one signing the tx
3414            let key_auth = KeyAuthorization {
3415                chain_id: 42431,
3416                key_type: SignatureType::Secp256k1,
3417                key_id: different_key_id, // Different from access_key_address
3418                expiry: None,
3419                limits: None,
3420            };
3421
3422            let auth_sig_hash = key_auth.signature_hash();
3423            let auth_signature = user_signer
3424                .sign_hash_sync(&auth_sig_hash)
3425                .expect("signing failed");
3426            let signed_key_auth =
3427                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3428
3429            // Transaction is signed by access_key_signer but KeyAuth has different_key_id
3430            let transaction = create_aa_with_keychain_signature(
3431                user_address,
3432                &access_key_signer,
3433                Some(signed_key_auth),
3434            );
3435
3436            let validator = setup_validator_with_keychain_storage(
3437                &transaction,
3438                user_address,
3439                different_key_id,
3440                None,
3441            );
3442            let mut state_provider = validator.inner.client().latest().unwrap();
3443
3444            let result = validate_against_keychain_default_fee_context(
3445                &validator,
3446                &transaction,
3447                &mut state_provider,
3448            );
3449            assert!(
3450                matches!(
3451                    result.expect("should not be a provider error"),
3452                    Err(TempoPoolTransactionError::Keychain(
3453                        "KeyAuthorization key_id does not match Keychain signature key_id"
3454                    ))
3455                ),
3456                "Mismatched key_id should be rejected"
3457            );
3458        }
3459
3460        #[test]
3461        fn test_key_authorization_invalid_signature_rejected() {
3462            let (access_key_signer, access_key_address) = generate_keypair();
3463            let (_user_signer, user_address) = generate_keypair();
3464            let (random_signer, _) = generate_keypair();
3465
3466            // Create KeyAuthorization but sign with a random key (not the user's key)
3467            let key_auth = KeyAuthorization {
3468                chain_id: 42431,
3469                key_type: SignatureType::Secp256k1,
3470                key_id: access_key_address,
3471                expiry: None,
3472                limits: None,
3473            };
3474
3475            let auth_sig_hash = key_auth.signature_hash();
3476            // Sign with random_signer instead of user_signer
3477            let auth_signature = random_signer
3478                .sign_hash_sync(&auth_sig_hash)
3479                .expect("signing failed");
3480            let signed_key_auth =
3481                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3482
3483            let transaction = create_aa_with_keychain_signature(
3484                user_address,
3485                &access_key_signer,
3486                Some(signed_key_auth),
3487            );
3488
3489            let validator = setup_validator_with_keychain_storage(
3490                &transaction,
3491                user_address,
3492                access_key_address,
3493                None,
3494            );
3495            let mut state_provider = validator.inner.client().latest().unwrap();
3496
3497            let result = validate_against_keychain_default_fee_context(
3498                &validator,
3499                &transaction,
3500                &mut state_provider,
3501            );
3502            assert!(
3503                matches!(
3504                    result.expect("should not be a provider error"),
3505                    Err(TempoPoolTransactionError::Keychain(
3506                        "Invalid KeyAuthorization signature"
3507                    ))
3508                ),
3509                "Invalid KeyAuthorization signature should be rejected"
3510            );
3511        }
3512
3513        #[test]
3514        fn test_keychain_user_address_mismatch_rejected() -> Result<(), ProviderError> {
3515            let (access_key_signer, access_key_address) = generate_keypair();
3516            let real_user = Address::random();
3517
3518            // Create transaction claiming to be from real_user
3519            let tx_aa = TempoTransaction {
3520                chain_id: 42431,
3521                max_priority_fee_per_gas: 1_000_000_000,
3522                max_fee_per_gas: 20_000_000_000,
3523                gas_limit: 1_000_000,
3524                calls: vec![Call {
3525                    to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
3526                    value: U256::ZERO,
3527                    input: alloy_primitives::Bytes::new(),
3528                }],
3529                nonce_key: U256::ZERO,
3530                nonce: 0,
3531                fee_token: Some(address!("0000000000000000000000000000000000000002")),
3532                fee_payer_signature: None,
3533                valid_after: None,
3534                valid_before: None,
3535                access_list: Default::default(),
3536                tempo_authorization_list: vec![],
3537                key_authorization: None,
3538            };
3539
3540            let unsigned = AASigned::new_unhashed(
3541                tx_aa.clone(),
3542                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3543                    Signature::test_signature(),
3544                )),
3545            );
3546            // V2: sign keccak256(0x04 || sig_hash || user_address)
3547            let sig_hash = KeychainSignature::signing_hash(unsigned.signature_hash(), real_user);
3548            let signature = access_key_signer
3549                .sign_hash_sync(&sig_hash)
3550                .expect("signing failed");
3551
3552            // Create keychain signature with DIFFERENT user_address than what sender() returns
3553            // The transaction's sender is derived from user_address in KeychainSignature
3554            let keychain_sig = TempoSignature::Keychain(KeychainSignature::new(
3555                real_user, // This becomes the sender
3556                PrimitiveSignature::Secp256k1(signature),
3557            ));
3558
3559            let signed_tx = AASigned::new_unhashed(tx_aa, keychain_sig);
3560            let envelope: TempoTxEnvelope = signed_tx.into();
3561            let recovered = envelope.try_into_recovered().unwrap();
3562            let transaction = TempoPooledTransaction::new(recovered);
3563
3564            // The transaction.sender() == real_user (from keychain sig's user_address)
3565            // So this validation path checks sig.user_address == transaction.sender()
3566            // which should always be true by construction.
3567            // The actual mismatch scenario would require manually constructing an invalid state.
3568
3569            // Setup with valid key for the actual sender
3570            let slot_value = AuthorizedKey {
3571                signature_type: 0,
3572                expiry: u64::MAX,
3573                enforce_limits: false,
3574                is_revoked: false,
3575            }
3576            .encode_to_slot();
3577            let validator = setup_validator_with_keychain_storage(
3578                &transaction,
3579                real_user,
3580                access_key_address,
3581                Some(slot_value),
3582            );
3583            let mut state_provider = validator.inner.client().latest().unwrap();
3584
3585            // This should pass since user_address matches sender by construction
3586            let result = validate_against_keychain_default_fee_context(
3587                &validator,
3588                &transaction,
3589                &mut state_provider,
3590            )?;
3591            assert!(
3592                result.is_ok(),
3593                "Properly constructed keychain sig should pass, got: {result:?}"
3594            );
3595            Ok(())
3596        }
3597
3598        /// Setup validator with keychain storage and a specific tip timestamp.
3599        fn setup_validator_with_keychain_storage_and_timestamp(
3600            transaction: &TempoPooledTransaction,
3601            user_address: Address,
3602            key_id: Address,
3603            authorized_key_slot_value: Option<U256>,
3604            tip_timestamp: u64,
3605        ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
3606            let provider =
3607                MockEthProvider::<TempoPrimitives>::new().with_chain_spec(moderato_with_t1c());
3608
3609            // Add sender account
3610            provider.add_account(
3611                transaction.sender(),
3612                ExtendedAccount::new(transaction.nonce(), U256::ZERO),
3613            );
3614
3615            // Create block with proper timestamp
3616            let block = Block {
3617                header: TempoHeader {
3618                    inner: Header {
3619                        timestamp: tip_timestamp,
3620                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
3621                        ..Default::default()
3622                    },
3623                    ..Default::default()
3624                },
3625                body: Default::default(),
3626            };
3627            provider.add_block(B256::random(), block);
3628
3629            // If slot value provided, setup AccountKeychain storage
3630            if let Some(slot_value) = authorized_key_slot_value {
3631                let storage_slot = AccountKeychain::new().keys[user_address][key_id].base_slot();
3632                provider.add_account(
3633                    ACCOUNT_KEYCHAIN_ADDRESS,
3634                    ExtendedAccount::new(0, U256::ZERO)
3635                        .extend_storage([(storage_slot.into(), slot_value)]),
3636                );
3637            }
3638
3639            let inner =
3640                EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
3641                    .disable_balance_check()
3642                    .build(InMemoryBlobStore::default());
3643            let amm_cache =
3644                AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
3645            let validator = TempoTransactionValidator::new(
3646                inner,
3647                DEFAULT_AA_VALID_AFTER_MAX_SECS,
3648                DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
3649                amm_cache,
3650            );
3651
3652            // Set the tip timestamp
3653            let mock_block = create_mock_block(tip_timestamp);
3654            validator.on_new_head_block(&mock_block);
3655
3656            validator
3657        }
3658
3659        #[test]
3660        fn test_stored_access_key_expired_rejected() {
3661            let (access_key_signer, access_key_address) = generate_keypair();
3662            let user_address = Address::random();
3663            let current_time = 1000u64;
3664
3665            let transaction =
3666                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
3667
3668            // Setup storage with an expired key (expiry in the past)
3669            let slot_value = AuthorizedKey {
3670                signature_type: 0,
3671                expiry: current_time - 1, // Expired (in the past)
3672                enforce_limits: false,
3673                is_revoked: false,
3674            }
3675            .encode_to_slot();
3676
3677            let validator = setup_validator_with_keychain_storage_and_timestamp(
3678                &transaction,
3679                user_address,
3680                access_key_address,
3681                Some(slot_value),
3682                current_time,
3683            );
3684            let mut state_provider = validator.inner.client().latest().unwrap();
3685
3686            let result = validate_against_keychain_default_fee_context(
3687                &validator,
3688                &transaction,
3689                &mut state_provider,
3690            );
3691            assert!(
3692                matches!(
3693                    result.expect("should not be a provider error"),
3694                    Err(TempoPoolTransactionError::AccessKeyExpired { expiry, min_allowed: ct })
3695                    if expiry == current_time - 1 && ct == current_time + AA_VALID_BEFORE_MIN_SECS
3696                ),
3697                "Expired access key should be rejected"
3698            );
3699        }
3700
3701        #[test]
3702        fn test_stored_access_key_expiry_at_current_time_rejected() {
3703            let (access_key_signer, access_key_address) = generate_keypair();
3704            let user_address = Address::random();
3705            let current_time = 1000u64;
3706
3707            let transaction =
3708                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
3709
3710            // Setup storage with expiry == current_time (edge case: expired)
3711            let slot_value = AuthorizedKey {
3712                signature_type: 0,
3713                expiry: current_time, // Expiry at exactly current time should be rejected
3714                enforce_limits: false,
3715                is_revoked: false,
3716            }
3717            .encode_to_slot();
3718
3719            let validator = setup_validator_with_keychain_storage_and_timestamp(
3720                &transaction,
3721                user_address,
3722                access_key_address,
3723                Some(slot_value),
3724                current_time,
3725            );
3726            let mut state_provider = validator.inner.client().latest().unwrap();
3727
3728            let result = validate_against_keychain_default_fee_context(
3729                &validator,
3730                &transaction,
3731                &mut state_provider,
3732            );
3733            assert!(
3734                matches!(
3735                    result.expect("should not be a provider error"),
3736                    Err(TempoPoolTransactionError::AccessKeyExpired { .. })
3737                ),
3738                "Access key with expiry == current_time should be rejected"
3739            );
3740        }
3741
3742        #[test]
3743        fn test_stored_access_key_valid_expiry_accepted() -> Result<(), ProviderError> {
3744            let (access_key_signer, access_key_address) = generate_keypair();
3745            let user_address = Address::random();
3746            let current_time = 1000u64;
3747
3748            let transaction =
3749                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
3750
3751            // Setup storage with a future expiry
3752            let slot_value = AuthorizedKey {
3753                signature_type: 0,
3754                expiry: current_time + 100, // Valid (in the future)
3755                enforce_limits: false,
3756                is_revoked: false,
3757            }
3758            .encode_to_slot();
3759
3760            let validator = setup_validator_with_keychain_storage_and_timestamp(
3761                &transaction,
3762                user_address,
3763                access_key_address,
3764                Some(slot_value),
3765                current_time,
3766            );
3767            let mut state_provider = validator.inner.client().latest().unwrap();
3768
3769            let result = validate_against_keychain_default_fee_context(
3770                &validator,
3771                &transaction,
3772                &mut state_provider,
3773            )?;
3774            assert!(
3775                result.is_ok(),
3776                "Access key with future expiry should be accepted, got: {result:?}"
3777            );
3778            Ok(())
3779        }
3780
3781        #[test]
3782        fn test_key_authorization_expired_rejected() {
3783            let (access_key_signer, access_key_address) = generate_keypair();
3784            let (user_signer, user_address) = generate_keypair();
3785            let current_time = 1000u64;
3786
3787            // Create KeyAuthorization with expired expiry
3788            let key_auth = KeyAuthorization {
3789                chain_id: 42431,
3790                key_type: SignatureType::Secp256k1,
3791                key_id: access_key_address,
3792                expiry: Some(current_time - 1), // Expired
3793                limits: None,
3794            };
3795
3796            let auth_sig_hash = key_auth.signature_hash();
3797            let auth_signature = user_signer
3798                .sign_hash_sync(&auth_sig_hash)
3799                .expect("signing failed");
3800            let signed_key_auth =
3801                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3802
3803            let transaction = create_aa_with_keychain_signature(
3804                user_address,
3805                &access_key_signer,
3806                Some(signed_key_auth),
3807            );
3808
3809            let validator = setup_validator_with_keychain_storage_and_timestamp(
3810                &transaction,
3811                user_address,
3812                access_key_address,
3813                None,
3814                current_time,
3815            );
3816            let mut state_provider = validator.inner.client().latest().unwrap();
3817
3818            let result = validate_against_keychain_default_fee_context(
3819                &validator,
3820                &transaction,
3821                &mut state_provider,
3822            );
3823            assert!(
3824                matches!(
3825                    result.expect("should not be a provider error"),
3826                    Err(TempoPoolTransactionError::KeyAuthorizationExpired { expiry, min_allowed: ct })
3827                    if expiry == current_time - 1 && ct == current_time + AA_VALID_BEFORE_MIN_SECS
3828                ),
3829                "Expired KeyAuthorization should be rejected"
3830            );
3831        }
3832
3833        #[test]
3834        fn test_key_authorization_expiry_at_current_time_rejected() {
3835            let (access_key_signer, access_key_address) = generate_keypair();
3836            let (user_signer, user_address) = generate_keypair();
3837            let current_time = 1000u64;
3838
3839            // Create KeyAuthorization with expiry == current_time
3840            let key_auth = KeyAuthorization {
3841                chain_id: 42431,
3842                key_type: SignatureType::Secp256k1,
3843                key_id: access_key_address,
3844                expiry: Some(current_time), // Expired at exactly current time
3845                limits: None,
3846            };
3847
3848            let auth_sig_hash = key_auth.signature_hash();
3849            let auth_signature = user_signer
3850                .sign_hash_sync(&auth_sig_hash)
3851                .expect("signing failed");
3852            let signed_key_auth =
3853                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3854
3855            let transaction = create_aa_with_keychain_signature(
3856                user_address,
3857                &access_key_signer,
3858                Some(signed_key_auth),
3859            );
3860
3861            let validator = setup_validator_with_keychain_storage_and_timestamp(
3862                &transaction,
3863                user_address,
3864                access_key_address,
3865                None,
3866                current_time,
3867            );
3868            let mut state_provider = validator.inner.client().latest().unwrap();
3869
3870            let result = validate_against_keychain_default_fee_context(
3871                &validator,
3872                &transaction,
3873                &mut state_provider,
3874            );
3875            assert!(
3876                matches!(
3877                    result.expect("should not be a provider error"),
3878                    Err(TempoPoolTransactionError::KeyAuthorizationExpired { .. })
3879                ),
3880                "KeyAuthorization with expiry == current_time should be rejected"
3881            );
3882        }
3883
3884        #[test]
3885        fn test_key_authorization_valid_expiry_accepted() -> Result<(), ProviderError> {
3886            let (access_key_signer, access_key_address) = generate_keypair();
3887            let (user_signer, user_address) = generate_keypair();
3888            let current_time = 1000u64;
3889
3890            // Create KeyAuthorization with future expiry
3891            let key_auth = KeyAuthorization {
3892                chain_id: 42431,
3893                key_type: SignatureType::Secp256k1,
3894                key_id: access_key_address,
3895                expiry: Some(current_time + 100), // Valid (in the future)
3896                limits: None,
3897            };
3898
3899            let auth_sig_hash = key_auth.signature_hash();
3900            let auth_signature = user_signer
3901                .sign_hash_sync(&auth_sig_hash)
3902                .expect("signing failed");
3903            let signed_key_auth =
3904                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3905
3906            let transaction = create_aa_with_keychain_signature(
3907                user_address,
3908                &access_key_signer,
3909                Some(signed_key_auth),
3910            );
3911
3912            let validator = setup_validator_with_keychain_storage_and_timestamp(
3913                &transaction,
3914                user_address,
3915                access_key_address,
3916                None,
3917                current_time,
3918            );
3919            let mut state_provider = validator.inner.client().latest().unwrap();
3920
3921            let result = validate_against_keychain_default_fee_context(
3922                &validator,
3923                &transaction,
3924                &mut state_provider,
3925            )?;
3926            assert!(
3927                result.is_ok(),
3928                "KeyAuthorization with future expiry should be accepted, got: {result:?}"
3929            );
3930            Ok(())
3931        }
3932
3933        #[test]
3934        fn test_key_authorization_expiry_cached_for_pool_maintenance() -> Result<(), ProviderError>
3935        {
3936            let (access_key_signer, access_key_address) = generate_keypair();
3937            let (user_signer, user_address) = generate_keypair();
3938            let current_time = 1000u64;
3939            let expiry = current_time + 100;
3940
3941            let key_auth = KeyAuthorization {
3942                chain_id: 42431,
3943                key_type: SignatureType::Secp256k1,
3944                key_id: access_key_address,
3945                expiry: Some(expiry),
3946                limits: None,
3947            };
3948
3949            let auth_sig_hash = key_auth.signature_hash();
3950            let auth_signature = user_signer
3951                .sign_hash_sync(&auth_sig_hash)
3952                .expect("signing failed");
3953            let signed_key_auth =
3954                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3955
3956            let transaction = create_aa_with_keychain_signature(
3957                user_address,
3958                &access_key_signer,
3959                Some(signed_key_auth),
3960            );
3961
3962            let validator = setup_validator_with_keychain_storage_and_timestamp(
3963                &transaction,
3964                user_address,
3965                access_key_address,
3966                None,
3967                current_time,
3968            );
3969            let mut state_provider = validator.inner.client().latest().unwrap();
3970
3971            let result = validate_against_keychain_default_fee_context(
3972                &validator,
3973                &transaction,
3974                &mut state_provider,
3975            )?;
3976            assert!(result.is_ok(), "KeyAuthorization should be accepted");
3977            assert_eq!(
3978                transaction.key_expiry(),
3979                Some(expiry),
3980                "KeyAuthorization expiry should be cached for pool maintenance"
3981            );
3982            Ok(())
3983        }
3984
3985        #[test]
3986        fn test_key_authorization_no_expiry_accepted() -> Result<(), ProviderError> {
3987            let (access_key_signer, access_key_address) = generate_keypair();
3988            let (user_signer, user_address) = generate_keypair();
3989            let current_time = 1000u64;
3990
3991            // Create KeyAuthorization with no expiry (None = never expires)
3992            let key_auth = KeyAuthorization {
3993                chain_id: 42431,
3994                key_type: SignatureType::Secp256k1,
3995                key_id: access_key_address,
3996                expiry: None, // Never expires
3997                limits: None,
3998            };
3999
4000            let auth_sig_hash = key_auth.signature_hash();
4001            let auth_signature = user_signer
4002                .sign_hash_sync(&auth_sig_hash)
4003                .expect("signing failed");
4004            let signed_key_auth =
4005                key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
4006
4007            let transaction = create_aa_with_keychain_signature(
4008                user_address,
4009                &access_key_signer,
4010                Some(signed_key_auth),
4011            );
4012
4013            let validator = setup_validator_with_keychain_storage_and_timestamp(
4014                &transaction,
4015                user_address,
4016                access_key_address,
4017                None,
4018                current_time,
4019            );
4020            let mut state_provider = validator.inner.client().latest().unwrap();
4021
4022            let result = validate_against_keychain_default_fee_context(
4023                &validator,
4024                &transaction,
4025                &mut state_provider,
4026            )?;
4027            assert!(
4028                result.is_ok(),
4029                "KeyAuthorization with no expiry should be accepted, got: {result:?}"
4030            );
4031            Ok(())
4032        }
4033
4034        /// Setup validator with keychain storage and spending limit for a specific user and key_id.
4035        fn setup_validator_with_spending_limit(
4036            transaction: &TempoPooledTransaction,
4037            user_address: Address,
4038            key_id: Address,
4039            enforce_limits: bool,
4040            spending_limit: Option<(Address, U256)>, // (token, limit)
4041        ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
4042            let provider =
4043                MockEthProvider::<TempoPrimitives>::new().with_chain_spec(moderato_with_t1c());
4044
4045            // Add sender account
4046            provider.add_account(
4047                transaction.sender(),
4048                ExtendedAccount::new(transaction.nonce(), U256::ZERO),
4049            );
4050            provider.add_block(B256::random(), Default::default());
4051
4052            // Setup AccountKeychain storage with AuthorizedKey
4053            let slot_value = AuthorizedKey {
4054                signature_type: 0,
4055                expiry: u64::MAX,
4056                enforce_limits,
4057                is_revoked: false,
4058            }
4059            .encode_to_slot();
4060
4061            let key_storage_slot = AccountKeychain::new().keys[user_address][key_id].base_slot();
4062            let mut storage = vec![(key_storage_slot.into(), slot_value)];
4063
4064            // Add spending limit if provided
4065            if let Some((token, limit)) = spending_limit {
4066                let limit_key = AccountKeychain::spending_limit_key(user_address, key_id);
4067                let limit_slot: U256 =
4068                    AccountKeychain::new().spending_limits[limit_key][token].slot();
4069                storage.push((limit_slot.into(), limit));
4070            }
4071
4072            provider.add_account(
4073                ACCOUNT_KEYCHAIN_ADDRESS,
4074                ExtendedAccount::new(0, U256::ZERO).extend_storage(storage),
4075            );
4076
4077            let inner =
4078                EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
4079                    .disable_balance_check()
4080                    .build(InMemoryBlobStore::default());
4081            let amm_cache =
4082                AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
4083            TempoTransactionValidator::new(
4084                inner,
4085                DEFAULT_AA_VALID_AFTER_MAX_SECS,
4086                DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
4087                amm_cache,
4088            )
4089        }
4090
4091        #[test]
4092        fn test_spending_limit_not_enforced_passes() -> Result<(), ProviderError> {
4093            let (access_key_signer, access_key_address) = generate_keypair();
4094            let user_address = Address::random();
4095
4096            let transaction =
4097                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4098
4099            // enforce_limits = false, no spending limit set
4100            let validator = setup_validator_with_spending_limit(
4101                &transaction,
4102                user_address,
4103                access_key_address,
4104                false, // enforce_limits = false
4105                None,  // no spending limit
4106            );
4107            let mut state_provider = validator.inner.client().latest().unwrap();
4108
4109            let result = validate_against_keychain_default_fee_context(
4110                &validator,
4111                &transaction,
4112                &mut state_provider,
4113            )?;
4114            assert!(
4115                result.is_ok(),
4116                "Key with enforce_limits=false should pass, got: {result:?}"
4117            );
4118            Ok(())
4119        }
4120
4121        #[test]
4122        fn test_spending_limit_sufficient_passes() -> Result<(), ProviderError> {
4123            let (access_key_signer, access_key_address) = generate_keypair();
4124            let user_address = Address::random();
4125
4126            let transaction =
4127                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4128
4129            // Get the fee token from the transaction
4130            let fee_token = transaction
4131                .inner()
4132                .fee_token()
4133                .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4134            let fee_cost = transaction.fee_token_cost();
4135
4136            // Set spending limit higher than fee cost
4137            let validator = setup_validator_with_spending_limit(
4138                &transaction,
4139                user_address,
4140                access_key_address,
4141                true,                                          // enforce_limits = true
4142                Some((fee_token, fee_cost + U256::from(100))), // limit > cost
4143            );
4144            let mut state_provider = validator.inner.client().latest().unwrap();
4145
4146            let result = validate_against_keychain_default_fee_context(
4147                &validator,
4148                &transaction,
4149                &mut state_provider,
4150            )?;
4151            assert!(
4152                result.is_ok(),
4153                "Sufficient spending limit should pass, got: {result:?}"
4154            );
4155            Ok(())
4156        }
4157
4158        #[test]
4159        fn test_spending_limit_exact_passes() -> Result<(), ProviderError> {
4160            let (access_key_signer, access_key_address) = generate_keypair();
4161            let user_address = Address::random();
4162
4163            let transaction =
4164                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4165
4166            let fee_token = transaction
4167                .inner()
4168                .fee_token()
4169                .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4170            let fee_cost = transaction.fee_token_cost();
4171
4172            // Set spending limit exactly equal to fee cost
4173            let validator = setup_validator_with_spending_limit(
4174                &transaction,
4175                user_address,
4176                access_key_address,
4177                true,                        // enforce_limits = true
4178                Some((fee_token, fee_cost)), // limit == cost
4179            );
4180            let mut state_provider = validator.inner.client().latest().unwrap();
4181
4182            let result = validate_against_keychain_default_fee_context(
4183                &validator,
4184                &transaction,
4185                &mut state_provider,
4186            )?;
4187            assert!(
4188                result.is_ok(),
4189                "Exact spending limit should pass, got: {result:?}"
4190            );
4191            Ok(())
4192        }
4193
4194        #[test]
4195        fn test_spending_limit_exceeded_rejected() {
4196            let (access_key_signer, access_key_address) = generate_keypair();
4197            let user_address = Address::random();
4198
4199            let transaction =
4200                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4201
4202            let fee_token = transaction
4203                .inner()
4204                .fee_token()
4205                .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4206            let fee_cost = transaction.fee_token_cost();
4207
4208            // Set spending limit lower than fee cost
4209            let insufficient_limit = fee_cost - U256::from(1);
4210            let validator = setup_validator_with_spending_limit(
4211                &transaction,
4212                user_address,
4213                access_key_address,
4214                true,                                  // enforce_limits = true
4215                Some((fee_token, insufficient_limit)), // limit < cost
4216            );
4217            let mut state_provider = validator.inner.client().latest().unwrap();
4218
4219            let result = validate_against_keychain_default_fee_context(
4220                &validator,
4221                &transaction,
4222                &mut state_provider,
4223            );
4224            assert!(
4225                matches!(
4226                    result.expect("should not be a provider error"),
4227                    Err(TempoPoolTransactionError::SpendingLimitExceeded { .. })
4228                ),
4229                "Insufficient spending limit should be rejected"
4230            );
4231        }
4232
4233        #[test]
4234        fn test_spending_limit_zero_rejected() {
4235            let (access_key_signer, access_key_address) = generate_keypair();
4236            let user_address = Address::random();
4237
4238            let transaction =
4239                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4240
4241            let fee_token = transaction
4242                .inner()
4243                .fee_token()
4244                .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4245
4246            // Set spending limit to zero (no spending limit set means zero)
4247            let validator = setup_validator_with_spending_limit(
4248                &transaction,
4249                user_address,
4250                access_key_address,
4251                true,                          // enforce_limits = true
4252                Some((fee_token, U256::ZERO)), // limit = 0
4253            );
4254            let mut state_provider = validator.inner.client().latest().unwrap();
4255
4256            let result = validate_against_keychain_default_fee_context(
4257                &validator,
4258                &transaction,
4259                &mut state_provider,
4260            );
4261            assert!(
4262                matches!(
4263                    result.expect("should not be a provider error"),
4264                    Err(TempoPoolTransactionError::SpendingLimitExceeded { .. })
4265                ),
4266                "Zero spending limit should be rejected"
4267            );
4268        }
4269
4270        #[test]
4271        fn test_spending_limit_wrong_token_rejected() {
4272            let (access_key_signer, access_key_address) = generate_keypair();
4273            let user_address = Address::random();
4274
4275            let transaction =
4276                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4277
4278            let fee_token = transaction
4279                .inner()
4280                .fee_token()
4281                .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4282
4283            // Set spending limit for a different token
4284            let different_token = Address::random();
4285            assert_ne!(fee_token, different_token); // Ensure they're different
4286
4287            let validator = setup_validator_with_spending_limit(
4288                &transaction,
4289                user_address,
4290                access_key_address,
4291                true,                               // enforce_limits = true
4292                Some((different_token, U256::MAX)), // High limit but for wrong token
4293            );
4294            let mut state_provider = validator.inner.client().latest().unwrap();
4295
4296            let result = validate_against_keychain_default_fee_context(
4297                &validator,
4298                &transaction,
4299                &mut state_provider,
4300            );
4301            assert!(
4302                matches!(
4303                    result.expect("should not be a provider error"),
4304                    Err(TempoPoolTransactionError::SpendingLimitExceeded { .. })
4305                ),
4306                "Wrong token spending limit should be rejected (fee token has 0 limit)"
4307            );
4308        }
4309
4310        /// Returns a MODERATO chain spec WITHOUT T1C (pre-T1C).
4311        fn moderato_without_t1c() -> TempoChainSpec {
4312            Arc::unwrap_or_clone(MODERATO.clone())
4313        }
4314
4315        /// Setup a validator with a specific chain spec and tip timestamp.
4316        fn setup_validator_with_spec(
4317            transaction: &TempoPooledTransaction,
4318            chain_spec: TempoChainSpec,
4319            tip_timestamp: u64,
4320        ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
4321            let provider = MockEthProvider::<TempoPrimitives>::new().with_chain_spec(chain_spec);
4322            provider.add_account(
4323                transaction.sender(),
4324                ExtendedAccount::new(transaction.nonce(), U256::ZERO),
4325            );
4326            provider.add_block(B256::random(), Default::default());
4327
4328            let inner =
4329                EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
4330                    .disable_balance_check()
4331                    .build(InMemoryBlobStore::default());
4332            let amm_cache =
4333                AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
4334            let validator = TempoTransactionValidator::new(
4335                inner,
4336                DEFAULT_AA_VALID_AFTER_MAX_SECS,
4337                DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
4338                amm_cache,
4339            );
4340
4341            let mock_block = create_mock_block(tip_timestamp);
4342            validator.on_new_head_block(&mock_block);
4343            validator
4344        }
4345
4346        #[test]
4347        fn test_legacy_v1_keychain_rejected_post_t1c() {
4348            let (access_key_signer, _) = generate_keypair();
4349            let user_address = Address::random();
4350
4351            let transaction =
4352                create_aa_with_v1_keychain_signature(user_address, &access_key_signer, None);
4353
4354            let validator = setup_validator_with_spec(&transaction, moderato_with_t1c(), 0);
4355            let spec = validator
4356                .inner
4357                .chain_spec()
4358                .tempo_hardfork_at(validator.inner.fork_tracker().tip_timestamp());
4359
4360            let result = validator.validate_keychain_version(&transaction, spec);
4361
4362            assert!(
4363                matches!(
4364                    result,
4365                    Err(TempoPoolTransactionError::LegacyKeychainPostT1C)
4366                ),
4367                "V1 keychain should be rejected post-T1C, got: {result:?}"
4368            );
4369        }
4370
4371        #[test]
4372        fn test_v2_keychain_accepted_post_t1c() -> Result<(), ProviderError> {
4373            let (access_key_signer, access_key_address) = generate_keypair();
4374            let user_address = Address::random();
4375
4376            let transaction =
4377                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4378
4379            let slot_value = AuthorizedKey {
4380                signature_type: 0,
4381                expiry: u64::MAX,
4382                enforce_limits: false,
4383                is_revoked: false,
4384            }
4385            .encode_to_slot();
4386
4387            let validator = setup_validator_with_keychain_storage(
4388                &transaction,
4389                user_address,
4390                access_key_address,
4391                Some(slot_value),
4392            );
4393            let mut state_provider = validator.inner.client().latest().unwrap();
4394
4395            let result = validate_against_keychain_default_fee_context(
4396                &validator,
4397                &transaction,
4398                &mut state_provider,
4399            )?;
4400            assert!(
4401                result.is_ok(),
4402                "V2 keychain should be accepted post-T1C, got: {result:?}"
4403            );
4404            Ok(())
4405        }
4406
4407        #[test]
4408        fn test_v2_keychain_rejected_pre_t1c() {
4409            let (access_key_signer, _) = generate_keypair();
4410            let user_address = Address::random();
4411
4412            let transaction =
4413                create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4414
4415            let validator = setup_validator_with_spec(&transaction, moderato_without_t1c(), 0);
4416            let spec = validator
4417                .inner
4418                .chain_spec()
4419                .tempo_hardfork_at(validator.inner.fork_tracker().tip_timestamp());
4420
4421            let result = validator.validate_keychain_version(&transaction, spec);
4422
4423            assert!(
4424                matches!(result, Err(TempoPoolTransactionError::V2KeychainPreT1C)),
4425                "V2 keychain should be rejected pre-T1C, got: {result:?}"
4426            );
4427        }
4428
4429        #[test]
4430        fn test_v1_keychain_accepted_pre_t1c() -> Result<(), ProviderError> {
4431            let (access_key_signer, access_key_address) = generate_keypair();
4432            let user_address = Address::random();
4433
4434            let transaction =
4435                create_aa_with_v1_keychain_signature(user_address, &access_key_signer, None);
4436
4437            let slot_value = AuthorizedKey {
4438                signature_type: 0,
4439                expiry: u64::MAX,
4440                enforce_limits: false,
4441                is_revoked: false,
4442            }
4443            .encode_to_slot();
4444
4445            // Pre-T1C validator with keychain storage
4446            let provider =
4447                MockEthProvider::<TempoPrimitives>::new().with_chain_spec(moderato_without_t1c());
4448            provider.add_account(
4449                transaction.sender(),
4450                ExtendedAccount::new(transaction.nonce(), U256::ZERO),
4451            );
4452            provider.add_block(B256::random(), Default::default());
4453            let storage_slot =
4454                AccountKeychain::new().keys[user_address][access_key_address].base_slot();
4455            provider.add_account(
4456                ACCOUNT_KEYCHAIN_ADDRESS,
4457                ExtendedAccount::new(0, U256::ZERO)
4458                    .extend_storage([(storage_slot.into(), slot_value)]),
4459            );
4460            let inner =
4461                EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
4462                    .disable_balance_check()
4463                    .build(InMemoryBlobStore::default());
4464            let amm_cache =
4465                AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
4466            let validator = TempoTransactionValidator::new(
4467                inner,
4468                DEFAULT_AA_VALID_AFTER_MAX_SECS,
4469                DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
4470                amm_cache,
4471            );
4472
4473            let mut state_provider = validator.inner.client().latest().unwrap();
4474            let result = validate_against_keychain_default_fee_context(
4475                &validator,
4476                &transaction,
4477                &mut state_provider,
4478            )?;
4479            assert!(
4480                result.is_ok(),
4481                "V1 keychain should be accepted pre-T1C, got: {result:?}"
4482            );
4483            Ok(())
4484        }
4485
4486        #[test]
4487        fn test_legacy_keychain_post_t1c_is_bad_transaction() {
4488            assert!(
4489                TempoPoolTransactionError::LegacyKeychainPostT1C.is_bad_transaction(),
4490                "Post-T1C V1 rejection should be a bad transaction (permanent)"
4491            );
4492        }
4493
4494        #[test]
4495        fn test_v2_keychain_pre_t1c_is_not_bad_transaction() {
4496            assert!(
4497                !TempoPoolTransactionError::V2KeychainPreT1C.is_bad_transaction(),
4498                "Pre-T1C V2 rejection should NOT be a bad transaction (transient)"
4499            );
4500        }
4501
4502        #[test]
4503        fn test_expired_access_key_is_not_bad_transaction() {
4504            assert!(
4505                !TempoPoolTransactionError::AccessKeyExpired {
4506                    expiry: 1,
4507                    min_allowed: 4,
4508                }
4509                .is_bad_transaction(),
4510                "Expired access key rejection should NOT be a bad transaction (timing-sensitive)"
4511            );
4512        }
4513
4514        #[test]
4515        fn test_expired_key_authorization_is_not_bad_transaction() {
4516            assert!(
4517                !TempoPoolTransactionError::KeyAuthorizationExpired {
4518                    expiry: 1,
4519                    min_allowed: 4,
4520                }
4521                .is_bad_transaction(),
4522                "Expired key authorization rejection should NOT be a bad transaction (timing-sensitive)"
4523            );
4524        }
4525    }
4526
4527    // ============================================
4528    // Authorization list limit tests
4529    // ============================================
4530
4531    /// Helper function to create an AA transaction with the given number of authorizations.
4532    fn create_aa_transaction_with_authorizations(
4533        authorization_count: usize,
4534    ) -> TempoPooledTransaction {
4535        use alloy_eips::eip7702::Authorization;
4536        use alloy_primitives::{Signature, TxKind, address};
4537        use tempo_primitives::transaction::{
4538            TempoSignedAuthorization, TempoTransaction,
4539            tempo_transaction::Call,
4540            tt_signature::{PrimitiveSignature, TempoSignature},
4541            tt_signed::AASigned,
4542        };
4543
4544        // Create dummy authorizations
4545        let authorizations: Vec<TempoSignedAuthorization> = (0..authorization_count)
4546            .map(|i| {
4547                let auth = Authorization {
4548                    chain_id: U256::from(1),
4549                    nonce: i as u64,
4550                    address: address!("0000000000000000000000000000000000000001"),
4551                };
4552                TempoSignedAuthorization::new_unchecked(
4553                    auth,
4554                    TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
4555                        Signature::test_signature(),
4556                    )),
4557                )
4558            })
4559            .collect();
4560
4561        let tx_aa = TempoTransaction {
4562            chain_id: 1,
4563            max_priority_fee_per_gas: 1_000_000_000,
4564            max_fee_per_gas: 20_000_000_000, // 20 gwei, above T1's minimum
4565            gas_limit: 1_000_000,
4566            calls: vec![Call {
4567                to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
4568                value: U256::ZERO,
4569                input: alloy_primitives::Bytes::new(),
4570            }],
4571            nonce_key: U256::ZERO,
4572            nonce: 0,
4573            fee_token: Some(address!("0000000000000000000000000000000000000002")),
4574            fee_payer_signature: None,
4575            valid_after: None,
4576            valid_before: None,
4577            access_list: Default::default(),
4578            tempo_authorization_list: authorizations,
4579            key_authorization: None,
4580        };
4581
4582        let signed_tx = AASigned::new_unhashed(
4583            tx_aa,
4584            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
4585        );
4586        let envelope: TempoTxEnvelope = signed_tx.into();
4587        let recovered = envelope.try_into_recovered().unwrap();
4588        TempoPooledTransaction::new(recovered)
4589    }
4590
4591    #[tokio::test]
4592    async fn test_aa_too_many_authorizations_rejected() {
4593        let current_time = std::time::SystemTime::now()
4594            .duration_since(std::time::UNIX_EPOCH)
4595            .unwrap()
4596            .as_secs();
4597
4598        // Create transaction with more authorizations than the default limit
4599        let transaction =
4600            create_aa_transaction_with_authorizations(DEFAULT_MAX_TEMPO_AUTHORIZATIONS + 1);
4601        let validator = setup_validator(&transaction, current_time);
4602
4603        let outcome = validator
4604            .validate_transaction(TransactionOrigin::External, transaction)
4605            .await;
4606
4607        match &outcome {
4608            TransactionValidationOutcome::Invalid(_, err) => {
4609                let error_msg = err.to_string();
4610                assert!(
4611                    error_msg.contains("Too many authorizations"),
4612                    "Expected TooManyAuthorizations error, got: {error_msg}"
4613                );
4614            }
4615            other => panic!("Expected Invalid outcome, got: {other:?}"),
4616        }
4617    }
4618
4619    #[tokio::test]
4620    async fn test_aa_authorization_count_at_limit_accepted() {
4621        let current_time = std::time::SystemTime::now()
4622            .duration_since(std::time::UNIX_EPOCH)
4623            .unwrap()
4624            .as_secs();
4625
4626        // Create transaction with exactly the limit
4627        let transaction =
4628            create_aa_transaction_with_authorizations(DEFAULT_MAX_TEMPO_AUTHORIZATIONS);
4629        let validator = setup_validator(&transaction, current_time);
4630
4631        let outcome = validator
4632            .validate_transaction(TransactionOrigin::External, transaction)
4633            .await;
4634
4635        // Should not fail with TooManyAuthorizations (may fail for other reasons)
4636        if let TransactionValidationOutcome::Invalid(_, err) = &outcome {
4637            let error_msg = err.to_string();
4638            assert!(
4639                !error_msg.contains("Too many authorizations"),
4640                "Should not fail with TooManyAuthorizations at the limit, got: {error_msg}"
4641            );
4642        }
4643    }
4644
4645    /// AA transactions must have at least one call.
4646    #[tokio::test]
4647    async fn test_aa_no_calls_rejected() {
4648        let current_time = std::time::SystemTime::now()
4649            .duration_since(std::time::UNIX_EPOCH)
4650            .unwrap()
4651            .as_secs();
4652
4653        // Create an AA transaction with no calls
4654        let transaction = TxBuilder::aa(Address::random())
4655            .fee_token(address!("0000000000000000000000000000000000000002"))
4656            .calls(vec![]) // Empty calls
4657            .build();
4658        let validator = setup_validator(&transaction, current_time);
4659
4660        let outcome = validator
4661            .validate_transaction(TransactionOrigin::External, transaction)
4662            .await;
4663
4664        match outcome {
4665            TransactionValidationOutcome::Invalid(_, ref err) => {
4666                assert!(
4667                    matches!(
4668                        err.downcast_other_ref::<TempoPoolTransactionError>(),
4669                        Some(TempoPoolTransactionError::NoCalls)
4670                    ),
4671                    "Expected NoCalls error, got: {err:?}"
4672                );
4673            }
4674            _ => panic!("Expected Invalid outcome with NoCalls error, got: {outcome:?}"),
4675        }
4676    }
4677
4678    /// CREATE calls (contract deployments) must be the first call in an AA transaction.
4679    #[tokio::test]
4680    async fn test_aa_create_call_not_first_rejected() {
4681        let current_time = std::time::SystemTime::now()
4682            .duration_since(std::time::UNIX_EPOCH)
4683            .unwrap()
4684            .as_secs();
4685
4686        // Create an AA transaction with a CREATE call as the second call
4687        let calls = vec![
4688            Call {
4689                to: TxKind::Call(Address::random()), // First call is a regular call
4690                value: U256::ZERO,
4691                input: Default::default(),
4692            },
4693            Call {
4694                to: TxKind::Create, // Second call is a CREATE - should be rejected
4695                value: U256::ZERO,
4696                input: Default::default(),
4697            },
4698        ];
4699
4700        let transaction = TxBuilder::aa(Address::random())
4701            .fee_token(address!("0000000000000000000000000000000000000002"))
4702            .calls(calls)
4703            .build();
4704        let validator = setup_validator(&transaction, current_time);
4705
4706        let outcome = validator
4707            .validate_transaction(TransactionOrigin::External, transaction)
4708            .await;
4709
4710        match outcome {
4711            TransactionValidationOutcome::Invalid(_, ref err) => {
4712                assert!(
4713                    matches!(
4714                        err.downcast_other_ref::<TempoPoolTransactionError>(),
4715                        Some(TempoPoolTransactionError::CreateCallNotFirst)
4716                    ),
4717                    "Expected CreateCallNotFirst error, got: {err:?}"
4718                );
4719            }
4720            _ => panic!("Expected Invalid outcome with CreateCallNotFirst error, got: {outcome:?}"),
4721        }
4722    }
4723
4724    /// CREATE call as the first call should be accepted.
4725    #[tokio::test]
4726    async fn test_aa_create_call_first_accepted() {
4727        let current_time = std::time::SystemTime::now()
4728            .duration_since(std::time::UNIX_EPOCH)
4729            .unwrap()
4730            .as_secs();
4731
4732        // Create an AA transaction with a CREATE call as the first call
4733        let calls = vec![
4734            Call {
4735                to: TxKind::Create, // First call is a CREATE - should be accepted
4736                value: U256::ZERO,
4737                input: Default::default(),
4738            },
4739            Call {
4740                to: TxKind::Call(Address::random()), // Second call is a regular call
4741                value: U256::ZERO,
4742                input: Default::default(),
4743            },
4744        ];
4745
4746        let transaction = TxBuilder::aa(Address::random())
4747            .fee_token(address!("0000000000000000000000000000000000000002"))
4748            .calls(calls)
4749            .build();
4750        let validator = setup_validator(&transaction, current_time);
4751
4752        let outcome = validator
4753            .validate_transaction(TransactionOrigin::External, transaction)
4754            .await;
4755
4756        // Should NOT fail with CreateCallNotFirst (may fail for other reasons)
4757        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
4758            assert!(
4759                !matches!(
4760                    err.downcast_other_ref::<TempoPoolTransactionError>(),
4761                    Some(TempoPoolTransactionError::CreateCallNotFirst)
4762                ),
4763                "CREATE call as first call should be accepted, got: {err:?}"
4764            );
4765        }
4766    }
4767
4768    /// Multiple CREATE calls in the same transaction should be rejected.
4769    #[tokio::test]
4770    async fn test_aa_multiple_creates_rejected() {
4771        let current_time = std::time::SystemTime::now()
4772            .duration_since(std::time::UNIX_EPOCH)
4773            .unwrap()
4774            .as_secs();
4775
4776        // calls = [CREATE, CALL, CREATE] -> should reject with CreateCallNotFirst
4777        let calls = vec![
4778            Call {
4779                to: TxKind::Create, // First call is a CREATE - ok
4780                value: U256::ZERO,
4781                input: Default::default(),
4782            },
4783            Call {
4784                to: TxKind::Call(Address::random()), // Second call is a regular call
4785                value: U256::ZERO,
4786                input: Default::default(),
4787            },
4788            Call {
4789                to: TxKind::Create, // Third call is a CREATE - should be rejected
4790                value: U256::ZERO,
4791                input: Default::default(),
4792            },
4793        ];
4794
4795        let transaction = TxBuilder::aa(Address::random())
4796            .fee_token(address!("0000000000000000000000000000000000000002"))
4797            .calls(calls)
4798            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
4799            .build();
4800        let validator = setup_validator(&transaction, current_time);
4801
4802        let outcome = validator
4803            .validate_transaction(TransactionOrigin::External, transaction)
4804            .await;
4805
4806        match outcome {
4807            TransactionValidationOutcome::Invalid(_, ref err) => {
4808                assert!(
4809                    matches!(
4810                        err.downcast_other_ref::<TempoPoolTransactionError>(),
4811                        Some(TempoPoolTransactionError::CreateCallNotFirst)
4812                    ),
4813                    "Expected CreateCallNotFirst error, got: {err:?}"
4814                );
4815            }
4816            _ => panic!("Expected Invalid outcome with CreateCallNotFirst error, got: {outcome:?}"),
4817        }
4818    }
4819
4820    /// CREATE calls must not have any entries in the authorization list.
4821    #[tokio::test]
4822    async fn test_aa_create_call_with_authorization_list_rejected() {
4823        use alloy_eips::eip7702::Authorization;
4824        use alloy_primitives::Signature;
4825        use tempo_primitives::transaction::{
4826            TempoSignedAuthorization,
4827            tt_signature::{PrimitiveSignature, TempoSignature},
4828        };
4829
4830        let current_time = std::time::SystemTime::now()
4831            .duration_since(std::time::UNIX_EPOCH)
4832            .unwrap()
4833            .as_secs();
4834
4835        // Create an AA transaction with a CREATE call and a non-empty authorization list
4836        let calls = vec![Call {
4837            to: TxKind::Create, // CREATE call
4838            value: U256::ZERO,
4839            input: Default::default(),
4840        }];
4841
4842        // Create a single authorization entry
4843        let auth = Authorization {
4844            chain_id: U256::from(1),
4845            nonce: 0,
4846            address: address!("0000000000000000000000000000000000000001"),
4847        };
4848        let authorization = TempoSignedAuthorization::new_unchecked(
4849            auth,
4850            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
4851        );
4852
4853        let transaction = TxBuilder::aa(Address::random())
4854            .fee_token(address!("0000000000000000000000000000000000000002"))
4855            .calls(calls)
4856            .authorization_list(vec![authorization])
4857            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
4858            .build();
4859        let validator = setup_validator(&transaction, current_time);
4860
4861        let outcome = validator
4862            .validate_transaction(TransactionOrigin::External, transaction)
4863            .await;
4864
4865        match outcome {
4866            TransactionValidationOutcome::Invalid(_, ref err) => {
4867                assert!(
4868                    matches!(
4869                        err.downcast_other_ref::<TempoPoolTransactionError>(),
4870                        Some(TempoPoolTransactionError::CreateCallWithAuthorizationList)
4871                    ),
4872                    "Expected CreateCallWithAuthorizationList error, got: {err:?}"
4873                );
4874            }
4875            _ => panic!(
4876                "Expected Invalid outcome with CreateCallWithAuthorizationList error, got: {outcome:?}"
4877            ),
4878        }
4879    }
4880
4881    /// Paused tokens should be rejected as invalid fee tokens.
4882    #[test]
4883    fn test_paused_token_is_invalid_fee_token() {
4884        let fee_token = address!("20C0000000000000000000000000000000000001");
4885
4886        // "USD" = 0x555344, stored in high bytes with length 6 (3*2) in LSB
4887        let usd_currency_value =
4888            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
4889
4890        let provider =
4891            MockEthProvider::default().with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
4892        provider.add_account(
4893            fee_token,
4894            ExtendedAccount::new(0, U256::ZERO).extend_storage([
4895                (tip20_slots::CURRENCY.into(), usd_currency_value),
4896                (tip20_slots::PAUSED.into(), U256::from(1)),
4897            ]),
4898        );
4899
4900        let mut state = provider.latest().unwrap();
4901        let spec = provider.chain_spec().tempo_hardfork_at(0);
4902
4903        // Test that is_fee_token_paused returns true for paused tokens
4904        let result = state.is_fee_token_paused(spec, fee_token);
4905        assert!(result.is_ok());
4906        assert!(
4907            result.unwrap(),
4908            "Paused tokens should be detected as paused"
4909        );
4910    }
4911
4912    /// Non-AA transaction with insufficient gas should be rejected with Invalid outcome
4913    /// and IntrinsicGasTooLow error.
4914    #[tokio::test]
4915    async fn test_non_aa_intrinsic_gas_insufficient_rejected() {
4916        let current_time = std::time::SystemTime::now()
4917            .duration_since(std::time::UNIX_EPOCH)
4918            .unwrap()
4919            .as_secs();
4920
4921        // Create EIP-1559 transaction with very low gas limit (below intrinsic gas of ~21k)
4922        let tx = TxBuilder::eip1559(Address::random())
4923            .gas_limit(1_000) // Way below intrinsic gas
4924            .build_eip1559();
4925
4926        let validator = setup_validator(&tx, current_time);
4927        let outcome = validator
4928            .validate_transaction(TransactionOrigin::External, tx)
4929            .await;
4930
4931        match outcome {
4932            TransactionValidationOutcome::Invalid(_, ref err) => {
4933                assert!(
4934                    matches!(err, InvalidPoolTransactionError::IntrinsicGasTooLow),
4935                    "Expected IntrinsicGasTooLow error, got: {err:?}"
4936                );
4937            }
4938            TransactionValidationOutcome::Error(_, _) => {
4939                panic!("Expected Invalid outcome, got Error - this was the bug we fixed!")
4940            }
4941            _ => panic!("Expected Invalid outcome with IntrinsicGasTooLow, got: {outcome:?}"),
4942        }
4943    }
4944
4945    /// Non-AA transaction with sufficient gas should pass intrinsic gas validation.
4946    #[tokio::test]
4947    async fn test_non_aa_intrinsic_gas_sufficient_passes() {
4948        let current_time = std::time::SystemTime::now()
4949            .duration_since(std::time::UNIX_EPOCH)
4950            .unwrap()
4951            .as_secs();
4952
4953        // Create EIP-1559 transaction with plenty of gas
4954        let tx = TxBuilder::eip1559(Address::random())
4955            .gas_limit(100_000) // Well above intrinsic gas
4956            .build_eip1559();
4957
4958        let validator = setup_validator(&tx, current_time);
4959        let outcome = validator
4960            .validate_transaction(TransactionOrigin::External, tx)
4961            .await;
4962
4963        // Should NOT fail with InsufficientGasForIntrinsicCost
4964        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
4965            assert!(
4966                matches!(err, InvalidPoolTransactionError::IntrinsicGasTooLow),
4967                "Non-AA tx with 100k gas should NOT fail intrinsic gas check, got: {err:?}"
4968            );
4969        }
4970    }
4971
4972    /// Non-AA transaction should NOT trigger AA-specific intrinsic gas error.
4973    /// This verifies the fix that gates AA intrinsic gas check to only AA transactions.
4974    #[tokio::test]
4975    async fn test_non_aa_tx_does_not_trigger_aa_intrinsic_gas_error() {
4976        let current_time = std::time::SystemTime::now()
4977            .duration_since(std::time::UNIX_EPOCH)
4978            .unwrap()
4979            .as_secs();
4980
4981        // Create EIP-1559 transaction with low gas
4982        let tx = TxBuilder::eip1559(Address::random())
4983            .gas_limit(1_000)
4984            .build_eip1559();
4985
4986        let validator = setup_validator(&tx, current_time);
4987        let outcome = validator
4988            .validate_transaction(TransactionOrigin::External, tx)
4989            .await;
4990
4991        // Should NOT get AA-specific error
4992        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
4993            assert!(
4994                !matches!(
4995                    err.downcast_other_ref::<TempoPoolTransactionError>(),
4996                    Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
4997                ),
4998                "Non-AA transaction should NOT trigger AA-specific intrinsic gas error"
4999            );
5000        }
5001    }
5002
5003    /// Verify intrinsic gas error is returned for insufficient gas.
5004    #[tokio::test]
5005    async fn test_intrinsic_gas_error_contains_gas_details() {
5006        let current_time = std::time::SystemTime::now()
5007            .duration_since(std::time::UNIX_EPOCH)
5008            .unwrap()
5009            .as_secs();
5010
5011        let gas_limit = 5_000u64;
5012        let tx = TxBuilder::eip1559(Address::random())
5013            .gas_limit(gas_limit)
5014            .build_eip1559();
5015
5016        let validator = setup_validator(&tx, current_time);
5017        let outcome = validator
5018            .validate_transaction(TransactionOrigin::External, tx)
5019            .await;
5020
5021        match outcome {
5022            TransactionValidationOutcome::Invalid(_, ref err) => {
5023                assert!(
5024                    matches!(err, InvalidPoolTransactionError::IntrinsicGasTooLow),
5025                    "Expected IntrinsicGasTooLow error, got: {err:?}"
5026                );
5027            }
5028            _ => panic!("Expected Invalid outcome, got: {outcome:?}"),
5029        }
5030    }
5031
5032    /// Paused validator tokens should be rejected even though they would bypass the liquidity check.
5033    #[test]
5034    fn test_paused_validator_token_rejected_before_liquidity_bypass() {
5035        // Use a TIP20-prefixed address for the fee token
5036        let paused_validator_token = address!("20C0000000000000000000000000000000000001");
5037
5038        // "USD" = 0x555344, stored in high bytes with length 6 (3*2) in LSB
5039        let usd_currency_value =
5040            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
5041
5042        let provider =
5043            MockEthProvider::default().with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
5044
5045        // Set up the token as a valid USD token but PAUSED
5046        provider.add_account(
5047            paused_validator_token,
5048            ExtendedAccount::new(0, U256::ZERO).extend_storage([
5049                (tip20_slots::CURRENCY.into(), usd_currency_value),
5050                (tip20_slots::PAUSED.into(), U256::from(1)),
5051            ]),
5052        );
5053
5054        let mut state = provider.latest().unwrap();
5055        let spec = provider.chain_spec().tempo_hardfork_at(0);
5056
5057        // Create AMM cache with the paused token in unique_tokens (simulating a validator's
5058        // preferred token). This would normally cause has_enough_liquidity() to return true
5059        // immediately at the bypass check.
5060        let amm_cache = AmmLiquidityCache::with_unique_tokens(vec![paused_validator_token]);
5061
5062        // Verify the bypass would apply: the token IS in unique_tokens
5063        assert!(
5064            amm_cache.is_active_validator_token(&paused_validator_token),
5065            "Token should be in unique_tokens for this test"
5066        );
5067
5068        // Verify has_enough_liquidity would bypass (return true) for this token
5069        // because it matches a validator token. This confirms the vulnerability we're testing.
5070        let liquidity_result =
5071            amm_cache.has_enough_liquidity(paused_validator_token, U256::from(1000), &state);
5072        assert!(
5073            liquidity_result.is_ok() && liquidity_result.unwrap(),
5074            "Token in unique_tokens should bypass liquidity check and return true"
5075        );
5076
5077        // BUT the pause check in is_fee_token_paused should catch it BEFORE the bypass
5078        let is_paused = state.is_fee_token_paused(spec, paused_validator_token);
5079        assert!(is_paused.is_ok());
5080        assert!(
5081            is_paused.unwrap(),
5082            "Paused validator token should be detected by is_fee_token_paused BEFORE reaching has_enough_liquidity"
5083        );
5084    }
5085
5086    #[tokio::test]
5087    async fn test_aa_exactly_max_calls_accepted() {
5088        let current_time = std::time::SystemTime::now()
5089            .duration_since(std::time::UNIX_EPOCH)
5090            .unwrap()
5091            .as_secs();
5092
5093        let calls: Vec<Call> = (0..MAX_AA_CALLS)
5094            .map(|_| Call {
5095                to: TxKind::Call(Address::random()),
5096                value: U256::ZERO,
5097                input: Default::default(),
5098            })
5099            .collect();
5100
5101        let transaction = TxBuilder::aa(Address::random())
5102            .fee_token(address!("0000000000000000000000000000000000000002"))
5103            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5104            .calls(calls)
5105            .build();
5106        let validator = setup_validator(&transaction, current_time);
5107
5108        let outcome = validator
5109            .validate_transaction(TransactionOrigin::External, transaction)
5110            .await;
5111
5112        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5113            assert!(
5114                !matches!(
5115                    err.downcast_other_ref::<TempoPoolTransactionError>(),
5116                    Some(TempoPoolTransactionError::TooManyCalls { .. })
5117                ),
5118                "Exactly MAX_AA_CALLS calls should not trigger TooManyCalls, got: {err:?}"
5119            );
5120        }
5121    }
5122
5123    #[tokio::test]
5124    async fn test_aa_too_many_calls_rejected() {
5125        let current_time = std::time::SystemTime::now()
5126            .duration_since(std::time::UNIX_EPOCH)
5127            .unwrap()
5128            .as_secs();
5129
5130        let calls: Vec<Call> = (0..MAX_AA_CALLS + 1)
5131            .map(|_| Call {
5132                to: TxKind::Call(Address::random()),
5133                value: U256::ZERO,
5134                input: Default::default(),
5135            })
5136            .collect();
5137
5138        let transaction = TxBuilder::aa(Address::random())
5139            .fee_token(address!("0000000000000000000000000000000000000002"))
5140            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5141            .calls(calls)
5142            .build();
5143        let validator = setup_validator(&transaction, current_time);
5144
5145        let outcome = validator
5146            .validate_transaction(TransactionOrigin::External, transaction)
5147            .await;
5148
5149        match outcome {
5150            TransactionValidationOutcome::Invalid(_, ref err) => {
5151                assert!(
5152                    matches!(
5153                        err.downcast_other_ref::<TempoPoolTransactionError>(),
5154                        Some(TempoPoolTransactionError::TooManyCalls { .. })
5155                    ),
5156                    "Expected TooManyCalls error, got: {err:?}"
5157                );
5158            }
5159            _ => panic!("Expected Invalid outcome with TooManyCalls error, got: {outcome:?}"),
5160        }
5161    }
5162
5163    #[tokio::test]
5164    async fn test_aa_exactly_max_call_input_size_accepted() {
5165        let current_time = std::time::SystemTime::now()
5166            .duration_since(std::time::UNIX_EPOCH)
5167            .unwrap()
5168            .as_secs();
5169
5170        let calls = vec![Call {
5171            to: TxKind::Call(Address::random()),
5172            value: U256::ZERO,
5173            input: vec![0u8; MAX_CALL_INPUT_SIZE].into(),
5174        }];
5175
5176        let transaction = TxBuilder::aa(Address::random())
5177            .fee_token(address!("0000000000000000000000000000000000000002"))
5178            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5179            .calls(calls)
5180            .build();
5181        let validator = setup_validator(&transaction, current_time);
5182
5183        let outcome = validator
5184            .validate_transaction(TransactionOrigin::External, transaction)
5185            .await;
5186
5187        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5188            assert!(
5189                !matches!(
5190                    err.downcast_other_ref::<TempoPoolTransactionError>(),
5191                    Some(TempoPoolTransactionError::CallInputTooLarge { .. })
5192                ),
5193                "Exactly MAX_CALL_INPUT_SIZE input should not trigger CallInputTooLarge, got: {err:?}"
5194            );
5195        }
5196    }
5197
5198    #[tokio::test]
5199    async fn test_aa_call_input_too_large_rejected() {
5200        let current_time = std::time::SystemTime::now()
5201            .duration_since(std::time::UNIX_EPOCH)
5202            .unwrap()
5203            .as_secs();
5204
5205        let calls = vec![Call {
5206            to: TxKind::Call(Address::random()),
5207            value: U256::ZERO,
5208            input: vec![0u8; MAX_CALL_INPUT_SIZE + 1].into(),
5209        }];
5210
5211        let transaction = TxBuilder::aa(Address::random())
5212            .fee_token(address!("0000000000000000000000000000000000000002"))
5213            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5214            .calls(calls)
5215            .build();
5216        let validator = setup_validator(&transaction, current_time);
5217
5218        let outcome = validator
5219            .validate_transaction(TransactionOrigin::External, transaction)
5220            .await;
5221
5222        match outcome {
5223            TransactionValidationOutcome::Invalid(_, ref err) => {
5224                let is_oversized = matches!(err, InvalidPoolTransactionError::OversizedData { .. });
5225                let is_call_input_too_large = matches!(
5226                    err.downcast_other_ref::<TempoPoolTransactionError>(),
5227                    Some(TempoPoolTransactionError::CallInputTooLarge { .. })
5228                );
5229                assert!(
5230                    is_oversized || is_call_input_too_large,
5231                    "Expected OversizedData or CallInputTooLarge error, got: {err:?}"
5232                );
5233            }
5234            _ => panic!("Expected Invalid outcome, got: {outcome:?}"),
5235        }
5236    }
5237
5238    #[tokio::test]
5239    async fn test_aa_exactly_max_access_list_accounts_accepted() {
5240        use alloy_eips::eip2930::{AccessList, AccessListItem};
5241
5242        let current_time = std::time::SystemTime::now()
5243            .duration_since(std::time::UNIX_EPOCH)
5244            .unwrap()
5245            .as_secs();
5246
5247        let items: Vec<AccessListItem> = (0..MAX_ACCESS_LIST_ACCOUNTS)
5248            .map(|_| AccessListItem {
5249                address: Address::random(),
5250                storage_keys: vec![],
5251            })
5252            .collect();
5253
5254        let transaction = TxBuilder::aa(Address::random())
5255            .fee_token(address!("0000000000000000000000000000000000000002"))
5256            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5257            .access_list(AccessList(items))
5258            .build();
5259        let validator = setup_validator(&transaction, current_time);
5260
5261        let outcome = validator
5262            .validate_transaction(TransactionOrigin::External, transaction)
5263            .await;
5264
5265        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5266            assert!(
5267                !matches!(
5268                    err.downcast_other_ref::<TempoPoolTransactionError>(),
5269                    Some(TempoPoolTransactionError::TooManyAccessListAccounts { .. })
5270                ),
5271                "Exactly MAX_ACCESS_LIST_ACCOUNTS should not trigger TooManyAccessListAccounts, got: {err:?}"
5272            );
5273        }
5274    }
5275
5276    #[tokio::test]
5277    async fn test_aa_too_many_access_list_accounts_rejected() {
5278        use alloy_eips::eip2930::{AccessList, AccessListItem};
5279
5280        let current_time = std::time::SystemTime::now()
5281            .duration_since(std::time::UNIX_EPOCH)
5282            .unwrap()
5283            .as_secs();
5284
5285        let items: Vec<AccessListItem> = (0..MAX_ACCESS_LIST_ACCOUNTS + 1)
5286            .map(|_| AccessListItem {
5287                address: Address::random(),
5288                storage_keys: vec![],
5289            })
5290            .collect();
5291
5292        let transaction = TxBuilder::aa(Address::random())
5293            .fee_token(address!("0000000000000000000000000000000000000002"))
5294            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5295            .access_list(AccessList(items))
5296            .build();
5297        let validator = setup_validator(&transaction, current_time);
5298
5299        let outcome = validator
5300            .validate_transaction(TransactionOrigin::External, transaction)
5301            .await;
5302
5303        match outcome {
5304            TransactionValidationOutcome::Invalid(_, ref err) => {
5305                assert!(
5306                    matches!(
5307                        err.downcast_other_ref::<TempoPoolTransactionError>(),
5308                        Some(TempoPoolTransactionError::TooManyAccessListAccounts { .. })
5309                    ),
5310                    "Expected TooManyAccessListAccounts error, got: {err:?}"
5311                );
5312            }
5313            _ => panic!(
5314                "Expected Invalid outcome with TooManyAccessListAccounts error, got: {outcome:?}"
5315            ),
5316        }
5317    }
5318
5319    #[tokio::test]
5320    async fn test_aa_exactly_max_storage_keys_per_account_accepted() {
5321        use alloy_eips::eip2930::{AccessList, AccessListItem};
5322
5323        let current_time = std::time::SystemTime::now()
5324            .duration_since(std::time::UNIX_EPOCH)
5325            .unwrap()
5326            .as_secs();
5327
5328        let items = vec![AccessListItem {
5329            address: Address::random(),
5330            storage_keys: (0..MAX_STORAGE_KEYS_PER_ACCOUNT)
5331                .map(|_| B256::random())
5332                .collect(),
5333        }];
5334
5335        let transaction = TxBuilder::aa(Address::random())
5336            .fee_token(address!("0000000000000000000000000000000000000002"))
5337            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5338            .access_list(AccessList(items))
5339            .build();
5340        let validator = setup_validator(&transaction, current_time);
5341
5342        let outcome = validator
5343            .validate_transaction(TransactionOrigin::External, transaction)
5344            .await;
5345
5346        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5347            assert!(
5348                !matches!(
5349                    err.downcast_other_ref::<TempoPoolTransactionError>(),
5350                    Some(TempoPoolTransactionError::TooManyStorageKeysPerAccount { .. })
5351                ),
5352                "Exactly MAX_STORAGE_KEYS_PER_ACCOUNT should not trigger TooManyStorageKeysPerAccount, got: {err:?}"
5353            );
5354        }
5355    }
5356
5357    #[tokio::test]
5358    async fn test_aa_too_many_storage_keys_per_account_rejected() {
5359        use alloy_eips::eip2930::{AccessList, AccessListItem};
5360
5361        let current_time = std::time::SystemTime::now()
5362            .duration_since(std::time::UNIX_EPOCH)
5363            .unwrap()
5364            .as_secs();
5365
5366        let items = vec![AccessListItem {
5367            address: Address::random(),
5368            storage_keys: (0..MAX_STORAGE_KEYS_PER_ACCOUNT + 1)
5369                .map(|_| B256::random())
5370                .collect(),
5371        }];
5372
5373        let transaction = TxBuilder::aa(Address::random())
5374            .fee_token(address!("0000000000000000000000000000000000000002"))
5375            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5376            .access_list(AccessList(items))
5377            .build();
5378        let validator = setup_validator(&transaction, current_time);
5379
5380        let outcome = validator
5381            .validate_transaction(TransactionOrigin::External, transaction)
5382            .await;
5383
5384        match outcome {
5385            TransactionValidationOutcome::Invalid(_, ref err) => {
5386                assert!(
5387                    matches!(
5388                        err.downcast_other_ref::<TempoPoolTransactionError>(),
5389                        Some(TempoPoolTransactionError::TooManyStorageKeysPerAccount { .. })
5390                    ),
5391                    "Expected TooManyStorageKeysPerAccount error, got: {err:?}"
5392                );
5393            }
5394            _ => panic!(
5395                "Expected Invalid outcome with TooManyStorageKeysPerAccount error, got: {outcome:?}"
5396            ),
5397        }
5398    }
5399
5400    #[tokio::test]
5401    async fn test_aa_exactly_max_total_storage_keys_accepted() {
5402        use alloy_eips::eip2930::{AccessList, AccessListItem};
5403
5404        let current_time = std::time::SystemTime::now()
5405            .duration_since(std::time::UNIX_EPOCH)
5406            .unwrap()
5407            .as_secs();
5408
5409        let keys_per_account = MAX_STORAGE_KEYS_PER_ACCOUNT;
5410        let num_accounts = MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL / keys_per_account;
5411        let items: Vec<AccessListItem> = (0..num_accounts)
5412            .map(|_| AccessListItem {
5413                address: Address::random(),
5414                storage_keys: (0..keys_per_account).map(|_| B256::random()).collect(),
5415            })
5416            .collect();
5417        assert_eq!(
5418            items.iter().map(|i| i.storage_keys.len()).sum::<usize>(),
5419            MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL
5420        );
5421
5422        let transaction = TxBuilder::aa(Address::random())
5423            .fee_token(address!("0000000000000000000000000000000000000002"))
5424            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5425            .access_list(AccessList(items))
5426            .build();
5427        let validator = setup_validator(&transaction, current_time);
5428
5429        let outcome = validator
5430            .validate_transaction(TransactionOrigin::External, transaction)
5431            .await;
5432
5433        if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5434            assert!(
5435                !matches!(
5436                    err.downcast_other_ref::<TempoPoolTransactionError>(),
5437                    Some(TempoPoolTransactionError::TooManyTotalStorageKeys { .. })
5438                ),
5439                "Exactly MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL should not trigger TooManyTotalStorageKeys, got: {err:?}"
5440            );
5441        }
5442    }
5443
5444    #[tokio::test]
5445    async fn test_aa_too_many_total_storage_keys_rejected() {
5446        use alloy_eips::eip2930::{AccessList, AccessListItem};
5447
5448        let current_time = std::time::SystemTime::now()
5449            .duration_since(std::time::UNIX_EPOCH)
5450            .unwrap()
5451            .as_secs();
5452
5453        let keys_per_account = MAX_STORAGE_KEYS_PER_ACCOUNT;
5454        let num_accounts = MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL / keys_per_account;
5455        let mut items: Vec<AccessListItem> = (0..num_accounts)
5456            .map(|_| AccessListItem {
5457                address: Address::random(),
5458                storage_keys: (0..keys_per_account).map(|_| B256::random()).collect(),
5459            })
5460            .collect();
5461        items.push(AccessListItem {
5462            address: Address::random(),
5463            storage_keys: vec![B256::random()],
5464        });
5465        assert_eq!(
5466            items.iter().map(|i| i.storage_keys.len()).sum::<usize>(),
5467            MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL + 1
5468        );
5469
5470        let transaction = TxBuilder::aa(Address::random())
5471            .fee_token(address!("0000000000000000000000000000000000000002"))
5472            .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5473            .access_list(AccessList(items))
5474            .build();
5475        let validator = setup_validator(&transaction, current_time);
5476
5477        let outcome = validator
5478            .validate_transaction(TransactionOrigin::External, transaction)
5479            .await;
5480
5481        match outcome {
5482            TransactionValidationOutcome::Invalid(_, ref err) => {
5483                assert!(
5484                    matches!(
5485                        err.downcast_other_ref::<TempoPoolTransactionError>(),
5486                        Some(TempoPoolTransactionError::TooManyTotalStorageKeys { .. })
5487                    ),
5488                    "Expected TooManyTotalStorageKeys error, got: {err:?}"
5489                );
5490            }
5491            _ => panic!(
5492                "Expected Invalid outcome with TooManyTotalStorageKeys error, got: {outcome:?}"
5493            ),
5494        }
5495    }
5496
5497    #[test]
5498    fn test_ensure_intrinsic_gas_tempo_tx_below_intrinsic_gas() {
5499        use tempo_chainspec::hardfork::TempoHardfork;
5500
5501        let tx = TxBuilder::eip1559(Address::random())
5502            .gas_limit(1)
5503            .build_eip1559();
5504
5505        let result = ensure_intrinsic_gas_tempo_tx(&tx, TempoHardfork::T1);
5506        assert!(
5507            matches!(result, Err(InvalidPoolTransactionError::IntrinsicGasTooLow)),
5508            "Expected IntrinsicGasTooLow, got: {result:?}"
5509        );
5510    }
5511
5512    #[test]
5513    fn test_ensure_intrinsic_gas_tempo_tx_exactly_at_intrinsic_gas() {
5514        use tempo_chainspec::hardfork::TempoHardfork;
5515        use tempo_revm::gas_params::tempo_gas_params;
5516
5517        let spec = TempoHardfork::T1;
5518        let tx_probe = TxBuilder::eip1559(Address::random())
5519            .gas_limit(1_000_000)
5520            .build_eip1559();
5521
5522        let gas_params = tempo_gas_params(spec);
5523        let mut gas = gas_params.initial_tx_gas(
5524            tx_probe.input(),
5525            tx_probe.is_create(),
5526            tx_probe.access_list().map(|l| l.len()).unwrap_or_default() as u64,
5527            tx_probe
5528                .access_list()
5529                .map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
5530                .unwrap_or_default() as u64,
5531            tx_probe
5532                .authorization_list()
5533                .map(|l| l.len())
5534                .unwrap_or_default() as u64,
5535        );
5536        if spec.is_t1() && tx_probe.nonce() == 0 {
5537            gas.initial_gas += gas_params.get(GasId::new_account_cost());
5538        }
5539        let intrinsic = std::cmp::max(gas.initial_gas, gas.floor_gas);
5540
5541        let tx_exact = TxBuilder::eip1559(Address::random())
5542            .gas_limit(intrinsic)
5543            .build_eip1559();
5544
5545        let result = ensure_intrinsic_gas_tempo_tx(&tx_exact, spec);
5546        assert!(
5547            result.is_ok(),
5548            "Gas limit exactly at intrinsic gas should pass, got: {result:?}"
5549        );
5550
5551        let tx_below = TxBuilder::eip1559(Address::random())
5552            .gas_limit(intrinsic - 1)
5553            .build_eip1559();
5554
5555        let result = ensure_intrinsic_gas_tempo_tx(&tx_below, spec);
5556        assert!(
5557            matches!(result, Err(InvalidPoolTransactionError::IntrinsicGasTooLow)),
5558            "Gas limit one below intrinsic gas should fail, got: {result:?}"
5559        );
5560    }
5561}