Skip to main content

tempo_revm/
handler.rs

1//! Tempo EVM Handler implementation.
2
3use std::{
4    cmp::Ordering,
5    fmt::Debug,
6    sync::{Arc, OnceLock},
7};
8
9use alloy_primitives::{Address, TxKind, U256};
10use reth_evm::{EvmError, EvmInternals};
11use revm::{
12    Database,
13    context::{
14        Block, Cfg, ContextTr, JournalTr, LocalContextTr, Transaction, TransactionType,
15        journaled_state::account::JournaledAccountTr,
16        result::{EVMError, ExecutionResult, InvalidTransaction, ResultGas},
17        transaction::{AccessListItem, AccessListItemTr},
18    },
19    context_interface::cfg::{GasId, GasParams},
20    handler::{
21        EvmTr, FrameResult, FrameTr, Handler, MainnetHandler,
22        pre_execution::{self, apply_auth_list, calculate_caller_fee},
23        validation,
24    },
25    inspector::{Inspector, InspectorHandler},
26    interpreter::{
27        Gas, InitialAndFloorGas,
28        gas::{
29            COLD_SLOAD_COST, STANDARD_TOKEN_COST, WARM_SSTORE_RESET,
30            get_tokens_in_calldata_istanbul,
31        },
32        interpreter::EthInterpreter,
33    },
34};
35use tempo_contracts::precompiles::{
36    IAccountKeychain::SignatureType as PrecompileSignatureType, TIPFeeAMMError,
37};
38use tempo_precompiles::{
39    ECRECOVER_GAS,
40    account_keychain::{AccountKeychain, TokenLimit, authorizeKeyCall},
41    error::TempoPrecompileError,
42    nonce::{EXPIRING_NONCE_MAX_EXPIRY_SECS, INonce::getNonceCall, NonceManager},
43    storage::{PrecompileStorageProvider, StorageCtx, evm::EvmPrecompileStorageProvider},
44    tip_fee_manager::TipFeeManager,
45    tip20::{ITIP20::InsufficientBalance, TIP20Error, TIP20Token, is_tip20_prefix},
46};
47use tempo_primitives::transaction::{
48    PrimitiveSignature, SignatureType, TEMPO_EXPIRING_NONCE_KEY, TempoSignature,
49    calc_gas_balance_spending, validate_calls,
50};
51
52use crate::{
53    TempoBatchCallEnv, TempoEvm, TempoInvalidTransaction, TempoTxEnv,
54    common::TempoStateAccess,
55    error::{FeePaymentError, TempoHaltReason},
56    evm::TempoContext,
57    gas_params::TempoGasParams,
58};
59
60/// Additional gas for P256 signature verification
61/// P256 precompile cost (6900 from EIP-7951) + 1100 for 129 bytes extra signature size - ecrecover savings (3000)
62const P256_VERIFY_GAS: u64 = 5_000;
63
64/// Additional gas for Keychain signatures (key validation overhead: COLD_SLOAD_COST + 900 processing)
65const KEYCHAIN_VALIDATION_GAS: u64 = COLD_SLOAD_COST + 900;
66
67/// Base gas for KeyAuthorization (22k storage + 5k buffer), signature gas added at runtime
68const KEY_AUTH_BASE_GAS: u64 = 27_000;
69
70/// Gas per spending limit in KeyAuthorization
71const KEY_AUTH_PER_LIMIT_GAS: u64 = 22_000;
72
73/// Gas cost for expiring nonce transactions (replay check + insert).
74///
75/// See [TIP-1009] for full specification.
76///
77/// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
78///
79/// Operations charged:
80/// - 2 cold SLOADs: `seen[tx_hash]`, `ring[idx]` (unique slots per tx)
81/// - 1 warm SLOAD: `seen[old_hash]` (warm because we just read `ring[idx]` which points to it)
82/// - 3 SSTOREs at RESET price: `seen[old_hash]=0`, `ring[idx]=tx_hash`, `seen[tx_hash]=valid_before`
83///
84/// Excluded from gas calculation:
85/// - `ring_ptr` SLOAD/SSTORE: Accessed by almost every expiring nonce tx in a block, so
86///   amortized cost approaches ~200 gas. May be moved out of EVM storage in the future.
87///
88/// Why SSTORE_RESET (2,900) instead of SSTORE_SET (20,000) for `seen[tx_hash]`:
89/// - SSTORE_SET cost exists to penalize permanent state growth
90/// - Expiring nonce data is ephemeral: evicted within 30 seconds, fixed-size buffer (300k)
91/// - No permanent state growth, so the 20k penalty doesn't apply
92///
93/// Total: 2*2100 + 100 + 3*2900 = 13,000 gas
94pub const EXPIRING_NONCE_GAS: u64 = 2 * COLD_SLOAD_COST + 100 + 3 * WARM_SSTORE_RESET;
95
96/// Calculates the gas cost for verifying a primitive signature.
97///
98/// Returns the additional gas required beyond the base transaction cost:
99/// - Secp256k1: 0 (already included in base 21k)
100/// - P256: 5000 gas
101/// - WebAuthn: 5000 gas + calldata cost for webauthn_data
102#[inline]
103fn primitive_signature_verification_gas(signature: &PrimitiveSignature) -> u64 {
104    match signature {
105        PrimitiveSignature::Secp256k1(_) => 0,
106        PrimitiveSignature::P256(_) => P256_VERIFY_GAS,
107        PrimitiveSignature::WebAuthn(webauthn_sig) => {
108            let tokens = get_tokens_in_calldata_istanbul(&webauthn_sig.webauthn_data);
109            P256_VERIFY_GAS + tokens * STANDARD_TOKEN_COST
110        }
111    }
112}
113
114/// Calculates the gas cost for verifying an AA signature.
115///
116/// For Keychain signatures, adds key validation overhead to the inner signature cost
117/// Returns the additional gas required beyond the base transaction cost.
118#[inline]
119fn tempo_signature_verification_gas(signature: &TempoSignature) -> u64 {
120    match signature {
121        TempoSignature::Primitive(prim_sig) => primitive_signature_verification_gas(prim_sig),
122        TempoSignature::Keychain(keychain_sig) => {
123            // Keychain = inner signature + key validation overhead (SLOAD + processing)
124            primitive_signature_verification_gas(&keychain_sig.signature) + KEYCHAIN_VALIDATION_GAS
125        }
126    }
127}
128
129/// Calculates the intrinsic gas cost for a KeyAuthorization.
130///
131/// This is charged before execution as part of transaction validation.
132///
133/// Pre-T1B: Gas = BASE (27k) + signature verification + (22k per spending limit)
134///   On T1/T1A this was double-charged alongside the gas-metered precompile call.
135///
136/// T1B+: Gas = signature verification + SLOAD (existing key check) +
137///   SSTORE (write key) + N × SSTORE (per spending limit)
138///   This is the sole gas accounting — the precompile runs with unlimited gas.
139#[inline]
140fn calculate_key_authorization_gas(
141    key_auth: &tempo_primitives::transaction::SignedKeyAuthorization,
142    gas_params: &GasParams,
143    spec: tempo_chainspec::hardfork::TempoHardfork,
144) -> u64 {
145    // All signature types pay ECRECOVER_GAS (3k) as the baseline since
146    // primitive_signature_verification_gas assumes ecrecover is already in base 21k.
147    // For KeyAuthorization, we're doing an additional signature verification.
148    let sig_gas = ECRECOVER_GAS + primitive_signature_verification_gas(&key_auth.signature);
149
150    let num_limits = key_auth
151        .authorization
152        .limits
153        .as_ref()
154        .map(|limits| limits.len() as u64)
155        .unwrap_or(0);
156
157    if spec.is_t1b() {
158        // T1B+: Accurate gas matching actual precompile storage operations.
159        // authorize_key does: 1 SLOAD (read existing key) + 1 SSTORE (write key)
160        //   + N SSTOREs (one per spending limit) + 2k buffer (TSTORE + keccak + event)
161        const BUFFER: u64 = 2_000;
162        let sstore_cost = gas_params.get(GasId::sstore_set_without_load_cost());
163        let sload_cost =
164            gas_params.warm_storage_read_cost() + gas_params.cold_storage_additional_cost();
165
166        sig_gas + sload_cost + sstore_cost * (1 + num_limits) + BUFFER
167    } else {
168        // Pre-T1B: Original heuristic constants
169        KEY_AUTH_BASE_GAS + sig_gas + num_limits * KEY_AUTH_PER_LIMIT_GAS
170    }
171}
172
173/// Computes the adjusted initial gas for AA transaction execution.
174///
175/// For T1+: Uses `evm_initial_gas` which includes key_authorization gas tracking.
176/// For pre-T1: Uses `init_and_floor_gas` directly to maintain backward compatibility,
177/// since pre-T1 doesn't have key_authorization gas tracking and Genesis has special
178/// handling where nonce_2d_gas is added to init_and_floor_gas but not to evm.initial_gas.
179#[inline]
180fn adjusted_initial_gas(
181    spec: tempo_chainspec::hardfork::TempoHardfork,
182    evm_initial_gas: u64,
183    init_and_floor_gas: &InitialAndFloorGas,
184) -> InitialAndFloorGas {
185    if spec.is_t1() {
186        InitialAndFloorGas::new(evm_initial_gas, init_and_floor_gas.floor_gas)
187    } else {
188        *init_and_floor_gas
189    }
190}
191
192/// Tempo EVM [`Handler`] implementation with Tempo specific modifications:
193///
194/// Fees are paid in fee tokens instead of account balance.
195#[derive(Debug)]
196pub struct TempoEvmHandler<DB, I> {
197    /// Fee token used for the transaction.
198    fee_token: Address,
199    /// Fee payer for the transaction.
200    fee_payer: Address,
201    /// Phantom data to avoid type inference issues.
202    _phantom: core::marker::PhantomData<(DB, I)>,
203}
204
205impl<DB, I> TempoEvmHandler<DB, I> {
206    /// Create a new [`TempoEvmHandler`] handler instance
207    pub fn new() -> Self {
208        Self {
209            fee_token: Address::default(),
210            fee_payer: Address::default(),
211            _phantom: core::marker::PhantomData,
212        }
213    }
214}
215
216impl<DB: alloy_evm::Database, I> TempoEvmHandler<DB, I> {
217    fn seed_tx_origin(
218        &self,
219        evm: &mut TempoEvm<DB, I>,
220    ) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
221        let ctx = evm.ctx_mut();
222
223        // Seed tx.origin in keychain transient storage for both regular execution and
224        // RPC simulations (`eth_call` / `eth_estimateGas`) that go through handler execution.
225        StorageCtx::enter_evm(
226            &mut ctx.journaled_state,
227            &ctx.block,
228            &ctx.cfg,
229            &ctx.tx,
230            || {
231                let mut keychain = AccountKeychain::new();
232                keychain.set_tx_origin(ctx.tx.caller())
233            },
234        )
235        .map_err(|e| EVMError::Custom(e.to_string()))
236    }
237
238    /// Loads the fee token and fee payer from the transaction environment.
239    ///
240    /// Resolves and validates the fee fields used by Tempo's fee system:
241    /// - Fee payer: determined from the transaction
242    /// - Fee token: resolved via the journaled state and validated (TIP20 prefix + USD currency)
243    ///
244    /// Must be called before `validate_against_state_and_deduct_caller`, which uses the
245    /// loaded fee fields for balance checks.
246    ///
247    /// Exposed for consumers like `FoundryHandler` that override the default run flow
248    /// but still need Tempo fee setup.
249    pub fn load_fee_fields(
250        &mut self,
251        evm: &mut TempoEvm<DB, I>,
252    ) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
253        let ctx = evm.ctx_mut();
254
255        self.fee_payer = ctx.tx.fee_payer()?;
256        if ctx.cfg.spec.is_t2()
257            && ctx.tx.has_fee_payer_signature()
258            && self.fee_payer == ctx.tx.caller()
259        {
260            return Err(TempoInvalidTransaction::SelfSponsoredFeePayer.into());
261        }
262
263        self.fee_token = ctx
264            .journaled_state
265            .get_fee_token(&ctx.tx, self.fee_payer, ctx.cfg.spec)
266            .map_err(|err| EVMError::Custom(err.to_string()))?;
267
268        // Always validate TIP20 prefix to prevent panics in get_token_balance.
269        // This is a protocol-level check since validators could bypass initial validation.
270        if !is_tip20_prefix(self.fee_token) {
271            return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into());
272        }
273
274        // Skip USD currency check for cases when the transaction is free and is not a part of a subblock.
275        // Since we already validated the TIP20 prefix above, we only need to check the USD currency.
276        if (!ctx.tx.max_balance_spending()?.is_zero() || ctx.tx.is_subblock_transaction())
277            && !ctx
278                .journaled_state
279                .is_tip20_usd(ctx.cfg.spec, self.fee_token)
280                .map_err(|err| EVMError::Custom(err.to_string()))?
281        {
282            return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into());
283        }
284
285        Ok(())
286    }
287}
288
289impl<DB, I> TempoEvmHandler<DB, I>
290where
291    DB: alloy_evm::Database,
292{
293    /// Generic single-call execution that works with both standard and inspector exec loops.
294    ///
295    /// This is the core implementation that both `execute_single_call` and inspector-aware
296    /// execution can use by providing the appropriate exec loop function.
297    fn execute_single_call_with<F>(
298        &mut self,
299        evm: &mut TempoEvm<DB, I>,
300        init_and_floor_gas: &InitialAndFloorGas,
301        mut run_loop: F,
302    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
303    where
304        F: FnMut(
305            &mut Self,
306            &mut TempoEvm<DB, I>,
307            <<TempoEvm<DB, I> as EvmTr>::Frame as FrameTr>::FrameInit,
308        ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
309    {
310        let gas_limit = evm.ctx().tx().gas_limit() - init_and_floor_gas.initial_gas;
311
312        // Create first frame action
313        let first_frame_input = self.first_frame_input(evm, gas_limit)?;
314
315        // Run execution loop (standard or inspector)
316        let mut frame_result = run_loop(self, evm, first_frame_input)?;
317
318        // Handle last frame result
319        self.last_frame_result(evm, &mut frame_result)?;
320
321        Ok(frame_result)
322    }
323
324    /// Executes a standard single-call transaction using the default handler logic.
325    ///
326    /// This calls the same helper methods used by the default [`Handler::execution`] implementation.
327    fn execute_single_call(
328        &mut self,
329        evm: &mut TempoEvm<DB, I>,
330        init_and_floor_gas: &InitialAndFloorGas,
331    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
332        self.execute_single_call_with(evm, init_and_floor_gas, Self::run_exec_loop)
333    }
334
335    /// Generic multi-call execution that works with both standard and inspector exec loops.
336    ///
337    /// This is the core implementation for atomic batch execution that both `execute_multi_call`
338    /// and inspector-aware execution can use by providing the appropriate single-call function.
339    ///
340    /// Provides atomic batch execution for AA transactions with multiple calls:
341    /// 1. Creates a checkpoint before executing any calls
342    /// 2. Executes each call sequentially, updating gas tracking
343    /// 3. If ANY call fails, reverts ALL state changes atomically
344    /// 4. If all calls succeed, commits ALL state changes atomically
345    ///
346    /// The atomicity is guaranteed by the checkpoint/revert/commit mechanism:
347    /// - Each individual call creates its own internal checkpoint
348    /// - The outer checkpoint (created here) captures state before any calls execute
349    /// - Reverting the outer checkpoint undoes all nested changes
350    fn execute_multi_call_with<F>(
351        &mut self,
352        evm: &mut TempoEvm<DB, I>,
353        init_and_floor_gas: &InitialAndFloorGas,
354        calls: Vec<tempo_primitives::transaction::Call>,
355        mut execute_single: F,
356    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
357    where
358        F: FnMut(
359            &mut Self,
360            &mut TempoEvm<DB, I>,
361            &InitialAndFloorGas,
362        ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
363    {
364        // Create checkpoint for atomic execution - captures state before any calls
365        let checkpoint = evm.ctx().journal_mut().checkpoint();
366
367        let gas_limit = evm.ctx().tx().gas_limit();
368        let mut remaining_gas = gas_limit - init_and_floor_gas.initial_gas;
369        let mut accumulated_gas_refund = 0i64;
370
371        // Store original TxEnv values to restore after batch execution
372        let original_kind = evm.ctx().tx().kind();
373        let original_value = evm.ctx().tx().value();
374        let original_data = evm.ctx().tx().input().clone();
375
376        let mut final_result = None;
377
378        for call in calls.iter() {
379            // Update TxEnv to point to this specific call
380            {
381                let tx = &mut evm.ctx().tx;
382                tx.inner.kind = call.to;
383                tx.inner.value = call.value;
384                tx.inner.data = call.input.clone();
385                tx.inner.gas_limit = remaining_gas;
386            }
387
388            // Execute call with NO additional initial gas (already deducted upfront in validation)
389            let zero_init_gas = InitialAndFloorGas::new(0, 0);
390            let frame_result = execute_single(self, evm, &zero_init_gas);
391
392            // Restore original TxEnv immediately after execution, even if execution failed
393            {
394                let tx = &mut evm.ctx().tx;
395                tx.inner.kind = original_kind;
396                tx.inner.value = original_value;
397                tx.inner.data = original_data.clone();
398                tx.inner.gas_limit = gas_limit;
399            }
400
401            let mut frame_result = frame_result?;
402
403            // Check if call succeeded
404            let instruction_result = frame_result.instruction_result();
405            if !instruction_result.is_ok() {
406                // Revert checkpoint - rolls back ALL state changes from ALL calls
407                evm.ctx().journal_mut().checkpoint_revert(checkpoint);
408
409                // For AA transactions with CREATE as the first call, the nonce was bumped by
410                // make_create_frame during execution. Since checkpoint_revert rolled that back,
411                // we need to manually bump the nonce here to ensure it persists even on failure.
412                //
413                // However, this only applies when using the protocol nonce (nonce_key == 0).
414                // When using 2D nonces (nonce_key != 0), replay protection is handled by the
415                // NonceManager, and the protocol nonce is only used for CREATE address derivation.
416                // Since the CREATE reverted, no contract was deployed, so the address wasn't
417                // "claimed" and we don't need to burn the protocol nonce.
418                let uses_protocol_nonce = evm
419                    .ctx()
420                    .tx()
421                    .tempo_tx_env
422                    .as_ref()
423                    .map(|aa| aa.nonce_key.is_zero())
424                    .unwrap_or(true);
425
426                if uses_protocol_nonce && calls.first().map(|c| c.to.is_create()).unwrap_or(false) {
427                    let caller = evm.ctx().tx().caller();
428                    if let Ok(mut caller_acc) =
429                        evm.ctx().journal_mut().load_account_with_code_mut(caller)
430                    {
431                        caller_acc.data.bump_nonce();
432                    }
433                }
434
435                // Include gas from all previous successful calls + failed call
436                let gas_spent_by_failed_call = frame_result.gas().spent();
437                let total_gas_spent = (gas_limit - remaining_gas) + gas_spent_by_failed_call;
438
439                // Create new Gas with correct limit, because Gas does not have a set_limit method
440                // (the frame_result has the limit from just the last call)
441                let mut corrected_gas = Gas::new(gas_limit);
442                if instruction_result.is_revert() {
443                    corrected_gas.set_spent(total_gas_spent);
444                } else {
445                    corrected_gas.spend_all();
446                }
447                corrected_gas.set_refund(0); // No refunds when batch fails and all state is reverted
448                *frame_result.gas_mut() = corrected_gas;
449
450                return Ok(frame_result);
451            }
452
453            // Call succeeded - accumulate gas usage and refunds
454            let gas_spent = frame_result.gas().spent();
455            let gas_refunded = frame_result.gas().refunded();
456
457            accumulated_gas_refund = accumulated_gas_refund.saturating_add(gas_refunded);
458            // Subtract only execution gas (intrinsic gas already deducted upfront)
459            remaining_gas = remaining_gas.saturating_sub(gas_spent);
460
461            final_result = Some(frame_result);
462        }
463
464        // All calls succeeded - commit checkpoint to finalize ALL state changes
465        evm.ctx().journal_mut().checkpoint_commit();
466
467        // Fix gas accounting for the entire batch
468        let mut result =
469            final_result.ok_or_else(|| EVMError::Custom("No calls executed".into()))?;
470
471        let total_gas_spent = gas_limit - remaining_gas;
472
473        // Create new Gas with correct limit, because Gas does not have a set_limit method
474        // (the frame_result has the limit from just the last call)
475        let mut corrected_gas = Gas::new(gas_limit);
476        corrected_gas.set_spent(total_gas_spent);
477        corrected_gas.set_refund(accumulated_gas_refund);
478        *result.gas_mut() = corrected_gas;
479
480        Ok(result)
481    }
482
483    /// Executes a multi-call AA transaction atomically.
484    fn execute_multi_call(
485        &mut self,
486        evm: &mut TempoEvm<DB, I>,
487        init_and_floor_gas: &InitialAndFloorGas,
488        calls: Vec<tempo_primitives::transaction::Call>,
489    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
490        self.execute_multi_call_with(evm, init_and_floor_gas, calls, Self::execute_single_call)
491    }
492
493    /// Executes a standard single-call transaction with inspector support.
494    ///
495    /// This is the inspector-aware version of execute_single_call that uses
496    /// inspect_run_exec_loop instead of run_exec_loop.
497    fn inspect_execute_single_call(
498        &mut self,
499        evm: &mut TempoEvm<DB, I>,
500        init_and_floor_gas: &InitialAndFloorGas,
501    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
502    where
503        I: Inspector<TempoContext<DB>, EthInterpreter>,
504    {
505        self.execute_single_call_with(evm, init_and_floor_gas, Self::inspect_run_exec_loop)
506    }
507
508    /// Executes a multi-call AA transaction atomically with inspector support.
509    ///
510    /// This is the inspector-aware version of execute_multi_call that uses
511    /// inspect_execute_single_call instead of execute_single_call.
512    fn inspect_execute_multi_call(
513        &mut self,
514        evm: &mut TempoEvm<DB, I>,
515        init_and_floor_gas: &InitialAndFloorGas,
516        calls: Vec<tempo_primitives::transaction::Call>,
517    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
518    where
519        I: Inspector<TempoContext<DB>, EthInterpreter>,
520    {
521        self.execute_multi_call_with(
522            evm,
523            init_and_floor_gas,
524            calls,
525            Self::inspect_execute_single_call,
526        )
527    }
528
529    /// Inspector-aware execution with a custom exec loop for standard (non-AA) transactions.
530    ///
531    /// Dispatches based on transaction type:
532    /// - AA transactions (type 0x76): Use batch execution path with calls field
533    /// - All other transactions: Use standard single-call execution
534    ///
535    /// This mirrors the logic in [`Handler::execution`] but uses inspector-aware execution methods.
536    ///
537    /// Additionally, delegates the standard single-call execution to the `exec_loop` closure.
538    /// This allows downstream consumers like the `FoundryHandler` to inject custom execution
539    /// loop logic (such as CREATE2 factory routing) while preserving all Tempo-specific
540    /// behavior as a single source of truth.
541    pub fn inspect_execution_with<F>(
542        &mut self,
543        evm: &mut TempoEvm<DB, I>,
544        init_and_floor_gas: &InitialAndFloorGas,
545        mut exec_loop: F,
546    ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
547    where
548        F: FnMut(
549            &mut Self,
550            &mut TempoEvm<DB, I>,
551            <<TempoEvm<DB, I> as EvmTr>::Frame as FrameTr>::FrameInit,
552        ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
553        I: Inspector<TempoContext<DB>, EthInterpreter>,
554    {
555        let spec = *evm.ctx_ref().cfg().spec();
556        let adjusted_gas = adjusted_initial_gas(spec, evm.initial_gas, init_and_floor_gas);
557
558        let tx = evm.tx();
559
560        if let Some(oog) = check_gas_limit(spec, tx, &adjusted_gas) {
561            return Ok(oog);
562        }
563
564        if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref() {
565            let calls = tempo_tx_env.aa_calls.clone();
566            return self.inspect_execute_multi_call(evm, &adjusted_gas, calls);
567        }
568
569        self.execute_single_call_with(evm, &adjusted_gas, &mut exec_loop)
570    }
571}
572
573impl<DB, I> Default for TempoEvmHandler<DB, I> {
574    fn default() -> Self {
575        Self::new()
576    }
577}
578
579impl<DB, I> Handler for TempoEvmHandler<DB, I>
580where
581    DB: alloy_evm::Database,
582{
583    type Evm = TempoEvm<DB, I>;
584    type Error = EVMError<DB::Error, TempoInvalidTransaction>;
585    type HaltReason = TempoHaltReason;
586
587    #[inline]
588    fn run(
589        &mut self,
590        evm: &mut Self::Evm,
591    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
592        self.load_fee_fields(evm)?;
593
594        // Standard handler flow - execution() handles single vs multi-call dispatch
595        match self.run_without_catch_error(evm) {
596            Ok(output) => Ok(output),
597            Err(err) => self.catch_error(evm, err),
598        }
599    }
600
601    /// Overridden execution method that handles AA vs standard transactions.
602    ///
603    /// Dispatches based on transaction type:
604    /// - AA transactions (type 0x5): Use batch execution path with calls field
605    /// - All other transactions: Use standard single-call execution
606    #[inline]
607    fn execution(
608        &mut self,
609        evm: &mut Self::Evm,
610        init_and_floor_gas: &InitialAndFloorGas,
611    ) -> Result<FrameResult, Self::Error> {
612        let spec = evm.ctx_ref().cfg().spec();
613        let adjusted_gas = adjusted_initial_gas(*spec, evm.initial_gas, init_and_floor_gas);
614        let tx = evm.tx();
615
616        if let Some(oog) = check_gas_limit(*spec, tx, &adjusted_gas) {
617            return Ok(oog);
618        }
619
620        if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref() {
621            let calls = tempo_tx_env.aa_calls.clone();
622            self.execute_multi_call(evm, &adjusted_gas, calls)
623        } else {
624            self.execute_single_call(evm, &adjusted_gas)
625        }
626    }
627
628    /// Take logs from the Journal if outcome is Halt Or Revert.
629    #[inline]
630    fn execution_result(
631        &mut self,
632        evm: &mut Self::Evm,
633        result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
634        result_gas: ResultGas,
635    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
636        evm.logs.clear();
637        // reset initial gas to 0 to avoid gas limit check errors
638        evm.initial_gas = 0;
639        if !result.instruction_result().is_ok() {
640            evm.logs = evm.journal_mut().take_logs();
641        }
642
643        MainnetHandler::default()
644            .execution_result(evm, result, result_gas)
645            .map(|result| result.map_haltreason(Into::into))
646    }
647
648    /// Override apply_eip7702_auth_list to support AA transactions with authorization lists.
649    ///
650    /// The default implementation only processes authorization lists for TransactionType::Eip7702 (0x04).
651    /// This override extends support to AA transactions (type 0x76) by checking for the presence
652    /// of an aa_authorization_list in the tempo_tx_env.
653    #[inline]
654    fn apply_eip7702_auth_list(&self, evm: &mut Self::Evm) -> Result<u64, Self::Error> {
655        let ctx = &mut evm.ctx;
656        let spec = ctx.cfg.spec;
657
658        // Check if this is an AA transaction with an authorization list
659        let has_aa_auth_list = ctx
660            .tx
661            .tempo_tx_env
662            .as_ref()
663            .map(|aa_env| !aa_env.tempo_authorization_list.is_empty())
664            .unwrap_or(false);
665
666        // If it's an AA transaction with authorization list, we need to apply it manually
667        // since the default implementation only checks for TransactionType::Eip7702
668        let refunded_gas = if has_aa_auth_list {
669            let tempo_tx_env = ctx.tx.tempo_tx_env.as_ref().unwrap();
670
671            apply_auth_list::<_, Self::Error>(
672                ctx.cfg.chain_id,
673                ctx.cfg.gas_params.tx_eip7702_auth_refund(),
674                tempo_tx_env
675                    .tempo_authorization_list
676                    .iter()
677                    // T0 hardfork: skip keychain signatures in auth list processing
678                    .filter(|auth| !(spec.is_t0() && auth.signature().is_keychain())),
679                &mut ctx.journaled_state,
680            )?
681        } else {
682            // For standard EIP-7702 transactions, use the default implementation
683            pre_execution::apply_eip7702_auth_list::<_, Self::Error>(evm.ctx())?
684        };
685
686        // TIP-1000: State Creation Cost Increase
687        // Authorization lists: There is no refund if the account already exists
688        if spec.is_t1() {
689            return Ok(0);
690        }
691
692        Ok(refunded_gas)
693    }
694
695    #[inline]
696    fn validate_against_state_and_deduct_caller(
697        &self,
698        evm: &mut Self::Evm,
699    ) -> Result<(), Self::Error> {
700        self.seed_tx_origin(evm)?;
701
702        let block = &evm.inner.ctx.block;
703        let tx = &evm.inner.ctx.tx;
704        let cfg = &evm.inner.ctx.cfg;
705        let journal = &mut evm.inner.ctx.journaled_state;
706
707        // Validate fee token has TIP20 prefix before loading balance.
708        // This prevents panics in get_token_balance for invalid fee tokens.
709        // Note: Full fee token validation (currency check) happens in load_fee_fields,
710        // but is skipped for free non-subblock transactions. This prefix check ensures
711        // we don't panic even for those cases.
712        if !is_tip20_prefix(self.fee_token) {
713            return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into());
714        }
715
716        // Load the fee payer balance
717        let account_balance = get_token_balance(journal, self.fee_token, self.fee_payer)?;
718
719        // Load caller's account
720        let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data;
721
722        let nonce_key = tx
723            .tempo_tx_env
724            .as_ref()
725            .map(|aa| aa.nonce_key)
726            .unwrap_or_default();
727
728        let spec = cfg.spec();
729
730        // Only treat as expiring nonce if T1 is active, otherwise treat as regular 2D nonce
731        let is_expiring_nonce = nonce_key == TEMPO_EXPIRING_NONCE_KEY && spec.is_t1();
732
733        // Validate account nonce and code (EIP-3607) using upstream helper
734        pre_execution::validate_account_nonce_and_code(
735            &caller_account.account().info,
736            tx.nonce(),
737            cfg.is_eip3607_disabled(),
738            // skip nonce check if 2D nonce or expiring nonce is used
739            cfg.is_nonce_check_disabled() || !nonce_key.is_zero(),
740        )?;
741
742        // modify account nonce and touch the account.
743        caller_account.touch();
744
745        // add additional gas for CREATE tx with 2d nonce and account nonce is 0.
746        // This case would create a new account for caller.
747        if !nonce_key.is_zero() && tx.kind().is_create() && caller_account.nonce() == 0 {
748            evm.initial_gas += cfg.gas_params().get(GasId::new_account_cost());
749
750            // do the gas limit check again.
751            if tx.gas_limit() < evm.initial_gas {
752                return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
753                    gas_limit: tx.gas_limit(),
754                    intrinsic_gas: evm.initial_gas,
755                }
756                .into());
757            }
758        }
759
760        if is_expiring_nonce {
761            // Expiring nonce transaction replay protection:
762            // - Pre-T1B: use tx_hash for backwards-compatible behavior.
763            // - T1B+: use expiring_nonce_hash (keccak256(encode_for_signing || sender))
764            //   to prevent replay via different fee payer signatures.
765            let tempo_tx_env = tx
766                .tempo_tx_env
767                .as_ref()
768                .ok_or(TempoInvalidTransaction::ExpiringNonceMissingTxEnv)?;
769
770            // Expiring nonce txs must have nonce == 0
771            if tx.nonce() != 0 {
772                return Err(TempoInvalidTransaction::ExpiringNonceNonceNotZero.into());
773            }
774
775            let replay_hash = if spec.is_t1b() {
776                tempo_tx_env
777                    .expiring_nonce_hash
778                    .ok_or(TempoInvalidTransaction::ExpiringNonceMissingTxEnv)?
779            } else {
780                tempo_tx_env.tx_hash
781            };
782            let valid_before = tempo_tx_env
783                .valid_before
784                .ok_or(TempoInvalidTransaction::ExpiringNonceMissingValidBefore)?;
785
786            let block_timestamp = block.timestamp().saturating_to::<u64>();
787            StorageCtx::enter_evm(journal, block, cfg, tx, || {
788                let mut nonce_manager = NonceManager::new();
789
790                nonce_manager
791                    .check_and_mark_expiring_nonce(replay_hash, valid_before)
792                    .map_err(|err| match err {
793                        TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
794                        TempoPrecompileError::NonceError(
795                            tempo_contracts::precompiles::NonceError::InvalidExpiringNonceExpiry(_),
796                        ) => {
797                            let max_allowed =
798                                block_timestamp.saturating_add(EXPIRING_NONCE_MAX_EXPIRY_SECS);
799                            if valid_before <= block_timestamp {
800                                TempoInvalidTransaction::NonceManagerError(format!(
801                                    "expiring nonce transaction expired: valid_before ({valid_before}) <= block timestamp ({block_timestamp})"
802                                ))
803                                .into()
804                            } else {
805                                TempoInvalidTransaction::NonceManagerError(format!(
806                                    "expiring nonce valid_before ({valid_before}) too far in the future: must be within {EXPIRING_NONCE_MAX_EXPIRY_SECS}s of block timestamp ({block_timestamp}), max allowed is {max_allowed}"
807                                ))
808                                .into()
809                            }
810                        }
811                        err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
812                    })?;
813
814                Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(())
815            })?;
816        } else if !nonce_key.is_zero() {
817            // 2D nonce transaction
818            StorageCtx::enter_evm(journal, block, cfg, tx, || {
819                let mut nonce_manager = NonceManager::new();
820
821                if !cfg.is_nonce_check_disabled() {
822                    let tx_nonce = tx.nonce();
823                    let state = nonce_manager
824                        .get_nonce(getNonceCall {
825                            account: tx.caller(),
826                            nonceKey: nonce_key,
827                        })
828                        .map_err(|err| match err {
829                            TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
830                            err => {
831                                TempoInvalidTransaction::NonceManagerError(err.to_string()).into()
832                            }
833                        })?;
834
835                    match tx_nonce.cmp(&state) {
836                        Ordering::Greater => {
837                            return Err(InvalidTransaction::NonceTooHigh {
838                                tx: tx_nonce,
839                                state,
840                            }
841                            .into());
842                        }
843                        Ordering::Less => {
844                            return Err(InvalidTransaction::NonceTooLow {
845                                tx: tx_nonce,
846                                state,
847                            }
848                            .into());
849                        }
850                        _ => {}
851                    }
852                }
853
854                // Always increment nonce for AA transactions with non-zero nonce keys.
855                nonce_manager
856                    .increment_nonce(tx.caller(), nonce_key)
857                    .map_err(|err| match err {
858                        TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
859                        err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
860                    })?;
861
862                Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(())
863            })?;
864        } else {
865            // Protocol nonce (nonce_key == 0)
866            // Bump the nonce for calls. Nonce for CREATE will be bumped in `make_create_frame`.
867            // This applies uniformly to both standard and AA transactions - we only bump here
868            // for CALLs, letting make_create_frame handle the nonce for CREATE operations.
869            if tx.kind().is_call() {
870                caller_account.bump_nonce();
871            }
872        }
873
874        // calculate the new balance after the fee is collected.
875        let new_balance = calculate_caller_fee(account_balance, tx, block, cfg)?;
876        // doing max to avoid underflow as new_balance can be more than account
877        // balance if `cfg.is_balance_check_disabled()` is true.
878        let gas_balance_spending = core::cmp::max(account_balance, new_balance) - new_balance;
879
880        // Note: Signature verification happens during recover_signer() before entering the pool
881        // Note: Transaction parameter validation (priority fee, time window) happens in validate_env()
882
883        // If the transaction includes a KeyAuthorization, validate and authorize the key
884        if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
885            && let Some(key_auth) = &tempo_tx_env.key_authorization
886        {
887            // Check if this TX is using a Keychain signature (access key)
888            // Access keys cannot authorize new keys UNLESS it's the same key being authorized (same-tx auth+use)
889            if let Some(keychain_sig) = tempo_tx_env.signature.as_keychain() {
890                // Use override_key_id if provided (for gas estimation), otherwise recover from signature
891                let access_key_addr = if let Some(override_key_id) = tempo_tx_env.override_key_id {
892                    override_key_id
893                } else {
894                    // Get the access key address (recovered during Tx->TxEnv conversion and cached)
895                    keychain_sig
896                        .key_id(&tempo_tx_env.signature_hash)
897                        .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
898                };
899
900                // Only allow if authorizing the same key that's being used (same-tx auth+use)
901                if access_key_addr != key_auth.key_id {
902                    return Err(TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys.into());
903                }
904            }
905
906            // Validate that the KeyAuthorization is signed by the root account
907            let root_account = &tx.caller;
908
909            // Recover the signer of the KeyAuthorization
910            let auth_signer = key_auth
911                .recover_signer()
912                .map_err(|_| TempoInvalidTransaction::KeyAuthorizationSignatureRecoveryFailed)?;
913
914            // Verify the KeyAuthorization is signed by the root account
915            if auth_signer != *root_account {
916                return Err(TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot {
917                    expected: *root_account,
918                    actual: auth_signer,
919                }
920                .into());
921            }
922
923            // Validate KeyAuthorization chain_id.
924            // T1C+: chain_id must exactly match (wildcard 0 is no longer allowed).
925            // Pre-T1C: chain_id == 0 allows replay on any chain (wildcard).
926            key_auth
927                .validate_chain_id(cfg.chain_id(), spec.is_t1c())
928                .map_err(TempoInvalidTransaction::from)?;
929
930            let keychain_checkpoint = if spec.is_t1() {
931                Some(journal.checkpoint())
932            } else {
933                None
934            };
935
936            let internals = EvmInternals::new(journal, block, cfg, tx);
937
938            // T1/T1A: Apply gas metering for the keychain precompile call.
939            // Pre-T1 and T1B+: Use unlimited gas.
940            // T1B+ disables gas metering here because gas is already accounted for
941            // in intrinsic gas via `calculate_key_authorization_gas`. Running with
942            // unlimited gas also eliminates the OOG path that caused the CREATE
943            // nonce replay vulnerability (protocol nonce not bumped on OOG).
944            let gas_limit = if spec.is_t1() && !spec.is_t1b() {
945                tx.gas_limit() - evm.initial_gas
946            } else {
947                u64::MAX
948            };
949
950            // Create gas_params with only sstore increase for key authorization
951            let gas_params = if spec.is_t1() {
952                static TABLE: OnceLock<GasParams> = OnceLock::new();
953                // only enabled SSTORE and warm storage read gas params for T1 fork in keychain.
954                TABLE
955                    .get_or_init(|| {
956                        let mut table = [0u64; 256];
957                        table[GasId::sstore_set_without_load_cost().as_usize()] =
958                            cfg.gas_params.get(GasId::sstore_set_without_load_cost());
959                        table[GasId::warm_storage_read_cost().as_usize()] =
960                            cfg.gas_params.get(GasId::warm_storage_read_cost());
961                        GasParams::new(Arc::new(table))
962                    })
963                    .clone()
964            } else {
965                cfg.gas_params.clone()
966            };
967
968            let mut provider = EvmPrecompileStorageProvider::new(
969                internals, gas_limit, cfg.spec, false, gas_params,
970            );
971
972            // The core logic of setting up thread-local storage is here.
973            let out_of_gas = StorageCtx::enter(&mut provider, || {
974                let mut keychain = AccountKeychain::default();
975                let access_key_addr = key_auth.key_id;
976
977                // Convert signature type to precompile SignatureType enum
978                // Use the key_type field which specifies the type of key being authorized
979                let signature_type = match key_auth.key_type {
980                    SignatureType::Secp256k1 => PrecompileSignatureType::Secp256k1,
981                    SignatureType::P256 => PrecompileSignatureType::P256,
982                    SignatureType::WebAuthn => PrecompileSignatureType::WebAuthn,
983                };
984
985                // Handle expiry: None means never expires (store as u64::MAX)
986                let expiry = key_auth.expiry.unwrap_or(u64::MAX);
987
988                // Validate expiry is not in the past
989                let current_timestamp = block.timestamp().saturating_to::<u64>();
990                if expiry <= current_timestamp {
991                    return Err(TempoInvalidTransaction::AccessKeyExpiryInPast {
992                        expiry,
993                        current_timestamp,
994                    }
995                    .into());
996                }
997
998                // Handle limits: None means unlimited spending (enforce_limits=false)
999                // Some([]) means no spending allowed (enforce_limits=true)
1000                // Some([...]) means specific limits (enforce_limits=true)
1001                let enforce_limits = key_auth.limits.is_some();
1002                let precompile_limits: Vec<TokenLimit> = key_auth
1003                    .limits
1004                    .as_ref()
1005                    .map(|limits| {
1006                        limits
1007                            .iter()
1008                            .map(|limit| TokenLimit {
1009                                token: limit.token,
1010                                amount: limit.limit,
1011                            })
1012                            .collect()
1013                    })
1014                    .unwrap_or_default();
1015
1016                // Create the authorize key call
1017                let authorize_call = authorizeKeyCall {
1018                    keyId: access_key_addr,
1019                    signatureType: signature_type,
1020                    expiry,
1021                    enforceLimits: enforce_limits,
1022                    limits: precompile_limits,
1023                };
1024
1025                // Call precompile to authorize the key (same phase as nonce increment)
1026                match keychain.authorize_key(*root_account, authorize_call) {
1027                    // all is good, we can do execution.
1028                    Ok(_) => Ok(false),
1029                    // on out of gas we are skipping execution but not invalidating the transaction.
1030                    Err(TempoPrecompileError::OutOfGas) => Ok(true),
1031                    Err(TempoPrecompileError::Fatal(err)) => Err(EVMError::Custom(err)),
1032                    Err(err) => Err(TempoInvalidTransaction::KeychainPrecompileError {
1033                        reason: err.to_string(),
1034                    }
1035                    .into()),
1036                }
1037            })?;
1038
1039            let gas_used = provider.gas_used();
1040            drop(provider);
1041
1042            // activated only on T1/T1A fork.
1043            // T1B+: Skip adding precompile gas to initial_gas since it is already
1044            // accounted for in intrinsic gas. The precompile runs with unlimited gas
1045            // on T1B+ so out_of_gas is never true.
1046            if let Some(keychain_checkpoint) = keychain_checkpoint {
1047                if spec.is_t1b() {
1048                    journal.checkpoint_commit();
1049                } else if out_of_gas {
1050                    evm.initial_gas = u64::MAX;
1051                    journal.checkpoint_revert(keychain_checkpoint);
1052                } else {
1053                    evm.initial_gas += gas_used;
1054                    journal.checkpoint_commit();
1055                };
1056            }
1057        }
1058
1059        // For Keychain signatures, validate that the keychain is authorized in the precompile
1060        // UNLESS this transaction also includes a KeyAuthorization (same-tx auth+use case)
1061        if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
1062            && let Some(keychain_sig) = tempo_tx_env.signature.as_keychain()
1063        {
1064            // Use override_key_id if provided (for gas estimation), otherwise recover from signature
1065            let access_key_addr = if let Some(override_key_id) = tempo_tx_env.override_key_id {
1066                override_key_id
1067            } else {
1068                // The user_address is the root account this transaction is being executed for
1069                // This should match tx.caller (which comes from recover_signer on the outer signature)
1070                let user_address = &keychain_sig.user_address;
1071
1072                // Sanity check: user_address should match tx.caller
1073                if *user_address != tx.caller {
1074                    return Err(TempoInvalidTransaction::KeychainUserAddressMismatch {
1075                        user_address: *user_address,
1076                        caller: tx.caller,
1077                    }
1078                    .into());
1079                }
1080
1081                // Get the access key address (recovered during pool validation and cached)
1082                keychain_sig
1083                    .key_id(&tempo_tx_env.signature_hash)
1084                    .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
1085            };
1086
1087            // Check if this transaction includes a KeyAuthorization for the same key
1088            // If so, skip keychain validation here - the key was just validated and authorized
1089            let is_authorizing_this_key = tempo_tx_env
1090                .key_authorization
1091                .as_ref()
1092                .map(|key_auth| key_auth.key_id == access_key_addr)
1093                .unwrap_or(false);
1094
1095            // Always need to set the transaction key for Keychain signatures
1096            StorageCtx::enter_precompile(
1097                journal,
1098                block,
1099                cfg,
1100                tx,
1101                |mut keychain: AccountKeychain| {
1102                    // Skip keychain validation when authorizing this key in the same tx
1103                    if !is_authorizing_this_key {
1104                        // Validate that user_address has authorized this access key in the keychain
1105                        let user_address = &keychain_sig.user_address;
1106
1107                        // Extract the signature type from the inner signature to validate it matches
1108                        // the key_type stored in the keychain. This prevents using a signature of one
1109                        // type to authenticate as a key registered with a different type.
1110                        // Only validate signature type on T1+ to maintain backward compatibility
1111                        // with historical blocks during re-execution.
1112                        let sig_type = spec
1113                            .is_t1()
1114                            .then_some(keychain_sig.signature.signature_type().into());
1115
1116                        keychain
1117                            .validate_keychain_authorization(
1118                                *user_address,
1119                                access_key_addr,
1120                                block.timestamp().to::<u64>(),
1121                                sig_type,
1122                            )
1123                            .map_err(|e| TempoInvalidTransaction::KeychainValidationFailed {
1124                                reason: format!("{e:?}"),
1125                            })?;
1126                    }
1127
1128                    // Set the transaction key in the keychain precompile
1129                    // This marks that the current transaction is using an access key
1130                    // The TIP20 precompile will read this during execution to enforce spending limits
1131                    keychain
1132                        .set_transaction_key(access_key_addr)
1133                        .map_err(|e| EVMError::Custom(e.to_string()))
1134                },
1135            )?;
1136        }
1137
1138        // Short-circuit if there is no spending for this transaction and `collectFeePreTx`
1139        // call will not collect any fees.
1140        if gas_balance_spending.is_zero() {
1141            return Ok(());
1142        }
1143
1144        let checkpoint = journal.checkpoint();
1145
1146        let result = StorageCtx::enter_evm(journal, &block, cfg, tx, || {
1147            TipFeeManager::new().collect_fee_pre_tx(
1148                self.fee_payer,
1149                self.fee_token,
1150                gas_balance_spending,
1151                block.beneficiary(),
1152            )
1153        });
1154
1155        if let Err(err) = result {
1156            // Revert the journal to checkpoint before `collectFeePreTx` call if something went wrong.
1157            journal.checkpoint_revert(checkpoint);
1158
1159            // Map fee collection errors to transaction validation errors since they
1160            // indicate the transaction cannot be included (e.g., insufficient liquidity
1161            // in FeeAMM pool for fee swaps)
1162            Err(match err {
1163                TempoPrecompileError::TIPFeeAMMError(TIPFeeAMMError::InsufficientLiquidity(_)) => {
1164                    FeePaymentError::InsufficientAmmLiquidity {
1165                        fee: gas_balance_spending,
1166                    }
1167                    .into()
1168                }
1169
1170                TempoPrecompileError::TIP20(TIP20Error::InsufficientBalance(
1171                    InsufficientBalance { available, .. },
1172                )) => FeePaymentError::InsufficientFeeTokenBalance {
1173                    fee: gas_balance_spending,
1174                    balance: available,
1175                }
1176                .into(),
1177
1178                TempoPrecompileError::Fatal(e) => EVMError::Custom(e),
1179
1180                _ => FeePaymentError::Other(err.to_string()).into(),
1181            })
1182        } else {
1183            journal.checkpoint_commit();
1184            evm.collected_fee = gas_balance_spending;
1185
1186            Ok(())
1187        }
1188    }
1189
1190    fn reimburse_caller(
1191        &self,
1192        evm: &mut Self::Evm,
1193        exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
1194    ) -> Result<(), Self::Error> {
1195        // Call collectFeePostTx on TipFeeManager precompile
1196        let context = &mut evm.inner.ctx;
1197        let tx = context.tx();
1198        let basefee = context.block().basefee() as u128;
1199        let effective_gas_price = tx.effective_gas_price(basefee);
1200        let gas = exec_result.gas();
1201
1202        let actual_spending = calc_gas_balance_spending(gas.used(), effective_gas_price);
1203        let refund_amount = tx.effective_balance_spending(
1204            context.block.basefee.into(),
1205            context.block.blob_gasprice().unwrap_or_default(),
1206        )? - tx.value
1207            - actual_spending;
1208
1209        // Skip `collectFeePostTx` call if the initial fee collected in
1210        // `collectFeePreTx` was zero, but spending is non-zero.
1211        //
1212        // This is normally unreachable unless the gas price was increased mid-transaction,
1213        // which is only possible when there are some EVM customizations involved (e.g Foundry EVM).
1214        if context.cfg.disable_fee_charge
1215            && evm.collected_fee.is_zero()
1216            && !actual_spending.is_zero()
1217        {
1218            return Ok(());
1219        }
1220
1221        // Create storage provider and fee manager
1222        let (journal, block, tx) = (&mut context.journaled_state, &context.block, &context.tx);
1223        let beneficiary = context.block.beneficiary();
1224
1225        StorageCtx::enter_evm(&mut *journal, block, &context.cfg, tx, || {
1226            let mut fee_manager = TipFeeManager::new();
1227
1228            if !actual_spending.is_zero() || !refund_amount.is_zero() {
1229                // Call collectFeePostTx (handles both refund and fee queuing)
1230                fee_manager
1231                    .collect_fee_post_tx(
1232                        self.fee_payer,
1233                        actual_spending,
1234                        refund_amount,
1235                        self.fee_token,
1236                        beneficiary,
1237                    )
1238                    .map_err(|e| EVMError::Custom(format!("{e:?}")))?;
1239            }
1240
1241            Ok(())
1242        })
1243    }
1244
1245    #[inline]
1246    fn reward_beneficiary(
1247        &self,
1248        _evm: &mut Self::Evm,
1249        _exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
1250    ) -> Result<(), Self::Error> {
1251        // Fee handling (refunds and swaps) are done in `reimburse_caller()` via `collectFeePostTx`.
1252        // Validators call distributeFees() to claim their accumulated fees.
1253        Ok(())
1254    }
1255
1256    /// Validates transaction environment with custom handling for AA transactions.
1257    ///
1258    /// Performs standard validation plus AA-specific checks:
1259    /// - Priority fee validation (EIP-1559)
1260    /// - Time window validation (validAfter/validBefore)
1261    #[inline]
1262    fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
1263        // All accounts have zero balance so transfer of value is not possible.
1264        // Check added in https://github.com/tempoxyz/tempo/pull/759
1265        if !evm.ctx.tx.value().is_zero() {
1266            return Err(TempoInvalidTransaction::ValueTransferNotAllowed.into());
1267        }
1268
1269        // First perform standard validation (header + transaction environment)
1270        // This validates: prevrandao, excess_blob_gas, chain_id, gas limits, tx type support, etc.
1271        validation::validate_env::<_, Self::Error>(evm.ctx())?;
1272
1273        // AA-specific validations
1274        let cfg = evm.ctx_ref().cfg();
1275        let tx = evm.ctx_ref().tx();
1276
1277        if let Some(aa_env) = tx.tempo_tx_env.as_ref() {
1278            // Validate AA transaction structure (calls list, CREATE rules)
1279            validate_calls(
1280                &aa_env.aa_calls,
1281                !aa_env.tempo_authorization_list.is_empty(),
1282            )
1283            .map_err(TempoInvalidTransaction::from)?;
1284
1285            // Validate keychain signature version (outer + authorization list).
1286            aa_env
1287                .signature
1288                .validate_version(cfg.spec().is_t1c())
1289                .map_err(TempoInvalidTransaction::from)?;
1290            for auth in &aa_env.tempo_authorization_list {
1291                auth.signature()
1292                    .validate_version(cfg.spec().is_t1c())
1293                    .map_err(TempoInvalidTransaction::from)?;
1294            }
1295
1296            let has_keychain_fields =
1297                aa_env.key_authorization.is_some() || aa_env.signature.is_keychain();
1298
1299            if aa_env.subblock_transaction && has_keychain_fields {
1300                return Err(TempoInvalidTransaction::KeychainOpInSubblockTransaction.into());
1301            }
1302
1303            // Validate priority fee for AA transactions using revm's validate_priority_fee_tx
1304            let base_fee = if cfg.is_base_fee_check_disabled() {
1305                None
1306            } else {
1307                Some(evm.ctx_ref().block().basefee() as u128)
1308            };
1309
1310            validation::validate_priority_fee_tx(
1311                tx.max_fee_per_gas(),
1312                tx.max_priority_fee_per_gas().unwrap_or_default(),
1313                base_fee,
1314                cfg.is_priority_fee_check_disabled(),
1315            )?;
1316
1317            // Validate time window for AA transactions
1318            let block_timestamp = evm.ctx_ref().block().timestamp().saturating_to();
1319            validate_time_window(aa_env.valid_after, aa_env.valid_before, block_timestamp)?;
1320        }
1321
1322        Ok(())
1323    }
1324
1325    /// Calculates initial gas costs with custom handling for AA transactions.
1326    ///
1327    /// AA transactions have variable intrinsic gas based on signature type:
1328    /// - secp256k1 (64/65 bytes): Standard 21k base
1329    /// - P256 (129 bytes): 21k base + 5k for P256 verification
1330    /// - WebAuthn (>129 bytes): 21k base + 5k + calldata gas for variable data
1331    #[inline]
1332    fn validate_initial_tx_gas(
1333        &self,
1334        evm: &mut Self::Evm,
1335    ) -> Result<InitialAndFloorGas, Self::Error> {
1336        let tx = evm.ctx_ref().tx();
1337        let spec = evm.ctx_ref().cfg().spec();
1338        let gas_params = evm.ctx_ref().cfg().gas_params();
1339        let gas_limit = tx.gas_limit();
1340
1341        // Route to appropriate gas calculation and validation based on transaction type
1342        let init_gas = if tx.tempo_tx_env.is_some() {
1343            // AA transaction - use batch gas calculation (includes validation)
1344            validate_aa_initial_tx_gas(evm)?
1345        } else {
1346            let mut acc = 0;
1347            let mut storage = 0;
1348            // legacy is only tx type that does not have access list.
1349            if tx.tx_type() != TransactionType::Legacy {
1350                (acc, storage) = tx
1351                    .access_list()
1352                    .map(|al| {
1353                        al.fold((0, 0), |(acc, storage), item| {
1354                            (acc + 1, storage + item.storage_slots().count())
1355                        })
1356                    })
1357                    .unwrap_or_default();
1358            };
1359            let mut init_gas = gas_params.initial_tx_gas(
1360                tx.input(),
1361                tx.kind().is_create(),
1362                acc as u64,
1363                storage as u64,
1364                tx.authorization_list_len() as u64,
1365            );
1366            // TIP-1000: Storage pricing updates for launch
1367            // EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas.
1368            // no need for v1 fork check as gas_params would be zero
1369            for auth in tx.authorization_list() {
1370                if auth.nonce == 0 {
1371                    init_gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost();
1372                }
1373            }
1374
1375            // TIP-1000: Storage pricing updates for launch
1376            // Transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas.
1377            if spec.is_t1() && tx.nonce == 0 {
1378                init_gas.initial_gas += gas_params.get(GasId::new_account_cost());
1379            }
1380
1381            if evm.ctx.cfg.is_eip7623_disabled() {
1382                init_gas.floor_gas = 0u64;
1383            }
1384
1385            // Validate gas limit is sufficient for initial gas
1386            if gas_limit < init_gas.initial_gas {
1387                return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1388                    gas_limit,
1389                    intrinsic_gas: init_gas.initial_gas,
1390                }
1391                .into());
1392            }
1393
1394            // Validate floor gas (Prague+)
1395            if !evm.ctx.cfg.is_eip7623_disabled() && gas_limit < init_gas.floor_gas {
1396                return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1397                    gas_limit,
1398                    intrinsic_gas: init_gas.floor_gas,
1399                }
1400                .into());
1401            }
1402
1403            init_gas
1404        };
1405
1406        // used to calculate key_authorization gas spending limit.
1407        evm.initial_gas = init_gas.initial_gas;
1408
1409        Ok(init_gas)
1410    }
1411
1412    fn catch_error(
1413        &self,
1414        evm: &mut Self::Evm,
1415        error: Self::Error,
1416    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
1417        // reset initial gas to 0 to avoid gas limit check errors
1418        evm.initial_gas = 0;
1419
1420        // For subblock transactions that failed `collectFeePreTx` call we catch error and treat such transactions as valid.
1421        if evm.ctx.tx.is_subblock_transaction()
1422            && let Some(
1423                TempoInvalidTransaction::CollectFeePreTx(_)
1424                | TempoInvalidTransaction::EthInvalidTransaction(
1425                    InvalidTransaction::LackOfFundForMaxFee { .. },
1426                ),
1427            ) = error.as_invalid_tx_err()
1428        {
1429            // Commit the transaction.
1430            //
1431            // `collectFeePreTx` call will happen after the nonce bump so this will only commit the nonce increment.
1432            evm.ctx.journaled_state.commit_tx();
1433
1434            evm.ctx().local_mut().clear();
1435            evm.frame_stack().clear();
1436
1437            Ok(ExecutionResult::Halt {
1438                reason: TempoHaltReason::SubblockTxFeePayment,
1439                logs: Default::default(),
1440                gas: ResultGas::default().with_limit(evm.ctx.tx.gas_limit),
1441            })
1442        } else {
1443            MainnetHandler::default()
1444                .catch_error(evm, error)
1445                .map(|result| result.map_haltreason(Into::into))
1446        }
1447    }
1448}
1449
1450/// Calculates intrinsic gas for an AA transaction batch using revm helpers.
1451///
1452/// This includes:
1453/// - Base 21k stipend (once for the transaction)
1454/// - Signature verification gas (P256: 5k, WebAuthn: 5k + webauthn_data)
1455/// - Per-call account access cost (COLD_ACCOUNT_ACCESS_COST * calls.len())
1456/// - Per-call input data gas (calldata tokens * 4 gas)
1457/// - Per-call CREATE costs (if applicable):
1458///   - Additional 32k base (CREATE constant)
1459///   - Initcode analysis gas (2 per 32-byte chunk, Shanghai+)
1460/// - Check that value transfer is zero.
1461/// - Access list costs (shared across batch)
1462/// - Key authorization costs (if present):
1463///   - Pre-T1B: 27k base + 3k ecrecover + 22k per spending limit
1464///   - T1B+: ecrecover + SLOAD + SSTORE × (1 + N limits)
1465/// - Floor gas calculation (EIP-7623, Prague+)
1466pub fn calculate_aa_batch_intrinsic_gas<'a>(
1467    aa_env: &TempoBatchCallEnv,
1468    gas_params: &GasParams,
1469    access_list: Option<impl Iterator<Item = &'a AccessListItem>>,
1470    spec: tempo_chainspec::hardfork::TempoHardfork,
1471) -> Result<InitialAndFloorGas, TempoInvalidTransaction> {
1472    let calls = &aa_env.aa_calls;
1473    let signature = &aa_env.signature;
1474    let authorization_list = &aa_env.tempo_authorization_list;
1475    let key_authorization = aa_env.key_authorization.as_ref();
1476    let mut gas = InitialAndFloorGas::default();
1477
1478    // 1. Base stipend (21k, once per transaction)
1479    gas.initial_gas += gas_params.tx_base_stipend();
1480
1481    // 2. Signature verification gas
1482    gas.initial_gas += tempo_signature_verification_gas(signature);
1483
1484    let cold_account_cost =
1485        gas_params.warm_storage_read_cost() + gas_params.cold_account_additional_cost();
1486
1487    // 3. Per-call overhead: cold account access
1488    // if the `to` address has not appeared in the call batch before.
1489    gas.initial_gas += cold_account_cost * calls.len().saturating_sub(1) as u64;
1490
1491    // 4. Authorization list costs (EIP-7702)
1492    gas.initial_gas +=
1493        authorization_list.len() as u64 * gas_params.tx_eip7702_per_empty_account_cost();
1494
1495    // Add signature verification costs for each authorization
1496    // No need for v1 fork check as gas_params would be zero
1497    for auth in authorization_list {
1498        gas.initial_gas += tempo_signature_verification_gas(auth.signature());
1499        // TIP-1000: Storage pricing updates for launch
1500        // EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas.
1501        if auth.nonce == 0 {
1502            gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost();
1503        }
1504    }
1505
1506    // 5. Key authorization costs (if present)
1507    if let Some(key_auth) = key_authorization {
1508        gas.initial_gas += calculate_key_authorization_gas(key_auth, gas_params, spec);
1509    }
1510
1511    // 6. Per-call costs
1512    let mut total_tokens = 0u64;
1513
1514    for call in calls {
1515        // 4a. Calldata gas using revm helper
1516        let tokens = get_tokens_in_calldata_istanbul(&call.input);
1517        total_tokens += tokens;
1518
1519        // 4b. CREATE-specific costs
1520        if call.to.is_create() {
1521            // CREATE costs 500,000 gas in TIP-1000 (T1), 32,000 before
1522            gas.initial_gas += gas_params.create_cost();
1523
1524            // EIP-3860: Initcode analysis gas using revm helper
1525            gas.initial_gas += gas_params.tx_initcode_cost(call.input.len());
1526        }
1527
1528        // Note: Transaction value is not allowed in AA transactions as there is no balances in accounts yet.
1529        // Check added in https://github.com/tempoxyz/tempo/pull/759
1530        if !call.value.is_zero() {
1531            return Err(TempoInvalidTransaction::ValueTransferNotAllowedInAATx);
1532        }
1533
1534        // 4c. Value transfer cost using revm constant
1535        // left here for future reference.
1536        if !call.value.is_zero() && call.to.is_call() {
1537            gas.initial_gas += gas_params.get(GasId::transfer_value_cost()); // 9000 gas
1538        }
1539    }
1540
1541    gas.initial_gas += total_tokens * gas_params.tx_token_cost();
1542
1543    // 5. Access list costs using revm constants
1544    if let Some(access_list) = access_list {
1545        let (accounts, storages) = access_list.fold((0, 0), |(acc_count, storage_count), item| {
1546            (acc_count + 1, storage_count + item.storage_slots().count())
1547        });
1548        gas.initial_gas += accounts * gas_params.tx_access_list_address_cost(); // 2400 per account
1549        gas.initial_gas += storages as u64 * gas_params.tx_access_list_storage_key_cost(); // 1900 per storage
1550    }
1551
1552    // 6. Floor gas using revm helper
1553    gas.floor_gas = gas_params.tx_floor_cost(total_tokens); // tokens * 10 + 21000
1554
1555    Ok(gas)
1556}
1557
1558/// Validates and calculates initial transaction gas for AA transactions.
1559///
1560/// Calculates intrinsic gas based on:
1561/// - Signature type (secp256k1: 21k, P256: 26k, WebAuthn: 26k + calldata)
1562/// - Batch call costs (per-call overhead, calldata, CREATE, value transfers)
1563fn validate_aa_initial_tx_gas<DB, I>(
1564    evm: &TempoEvm<DB, I>,
1565) -> Result<InitialAndFloorGas, EVMError<DB::Error, TempoInvalidTransaction>>
1566where
1567    DB: alloy_evm::Database,
1568{
1569    let (_, tx, cfg, _, _, _, _) = evm.ctx_ref().all();
1570    let gas_limit = tx.gas_limit();
1571    let gas_params = cfg.gas_params();
1572    let spec = *cfg.spec();
1573
1574    // This function should only be called for AA transactions
1575    let aa_env = tx
1576        .tempo_tx_env
1577        .as_ref()
1578        .expect("validate_aa_initial_tx_gas called for non-AA transaction");
1579
1580    let calls = &aa_env.aa_calls;
1581
1582    // Validate all CREATE calls' initcode size upfront (EIP-3860)
1583    let max_initcode_size = evm.ctx_ref().cfg().max_initcode_size();
1584    for call in calls {
1585        if call.to.is_create() && call.input.len() > max_initcode_size {
1586            return Err(InvalidTransaction::CreateInitCodeSizeLimit.into());
1587        }
1588    }
1589
1590    // Calculate batch intrinsic gas using helper
1591    let mut batch_gas =
1592        calculate_aa_batch_intrinsic_gas(aa_env, gas_params, tx.access_list(), spec)?;
1593
1594    let mut nonce_2d_gas = 0;
1595
1596    // Calculate 2D nonce gas if nonce_key is non-zero
1597    // If tx nonce is 0, it's a new key (0 -> 1 transition), otherwise existing key
1598    if spec.is_t1() {
1599        if aa_env.nonce_key == TEMPO_EXPIRING_NONCE_KEY {
1600            // Calculate nonce gas based on nonce type:
1601            // - Expiring nonce (nonce_key == MAX, T1 active): ring buffer + seen mapping operations
1602            // - 2D nonce (nonce_key != 0): SLOAD + SSTORE for nonce increment
1603            // - Regular nonce (nonce_key == 0): no additional gas
1604            batch_gas.initial_gas += EXPIRING_NONCE_GAS;
1605        } else if tx.nonce == 0 {
1606            // TIP-1000: Storage pricing updates for launch
1607            // Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas
1608            batch_gas.initial_gas += gas_params.get(GasId::new_account_cost());
1609        } else if !aa_env.nonce_key.is_zero() {
1610            // Existing 2D nonce key usage (nonce > 0)
1611            // TIP-1000 Invariant 3: existing state updates must charge +5,000 gas
1612            batch_gas.initial_gas += spec.gas_existing_nonce_key();
1613        }
1614    } else if let Some(aa_env) = &tx.tempo_tx_env
1615        && !aa_env.nonce_key.is_zero()
1616    {
1617        nonce_2d_gas = if tx.nonce() == 0 {
1618            spec.gas_new_nonce_key()
1619        } else {
1620            spec.gas_existing_nonce_key()
1621        };
1622    };
1623
1624    if evm.ctx.cfg.is_eip7623_disabled() {
1625        batch_gas.floor_gas = 0u64;
1626    }
1627
1628    // For T0+, include 2D nonce gas in validation (charged upfront)
1629    // For pre-T0 (Genesis), 2D nonce gas is added AFTER validation to allow transactions
1630    // with gas_limit < intrinsic + nonce_2d_gas to pass validation, but the gas is still
1631    // charged during execution via init_and_floor_gas (not evm.initial_gas)
1632    if spec.is_t0() {
1633        batch_gas.initial_gas += nonce_2d_gas;
1634    }
1635
1636    // Validate gas limit is sufficient for initial gas
1637    if gas_limit < batch_gas.initial_gas {
1638        return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1639            gas_limit,
1640            intrinsic_gas: batch_gas.initial_gas,
1641        }
1642        .into());
1643    }
1644
1645    // For pre-T0 (Genesis), add 2D nonce gas after validation
1646    // This gas will be charged via init_and_floor_gas, not evm.initial_gas
1647    if !spec.is_t0() {
1648        batch_gas.initial_gas += nonce_2d_gas;
1649    }
1650
1651    // Validate floor gas (Prague+)
1652    if !evm.ctx.cfg.is_eip7623_disabled() && gas_limit < batch_gas.floor_gas {
1653        return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1654            gas_limit,
1655            intrinsic_gas: batch_gas.floor_gas,
1656        }
1657        .into());
1658    }
1659
1660    Ok(batch_gas)
1661}
1662
1663/// IMPORTANT: the caller must ensure `token` is a valid TIP20Token address.
1664pub fn get_token_balance<JOURNAL>(
1665    journal: &mut JOURNAL,
1666    token: Address,
1667    sender: Address,
1668) -> Result<U256, <JOURNAL::Database as Database>::Error>
1669where
1670    JOURNAL: JournalTr,
1671{
1672    // Address has already been validated as having TIP20 prefix
1673    journal.load_account(token)?;
1674    let balance_slot = TIP20Token::from_address(token)
1675        .expect("TIP20 prefix already validated")
1676        .balances[sender]
1677        .slot();
1678    let balance = journal.sload(token, balance_slot)?.data;
1679
1680    Ok(balance)
1681}
1682
1683impl<DB, I> InspectorHandler for TempoEvmHandler<DB, I>
1684where
1685    DB: alloy_evm::Database,
1686    I: Inspector<TempoContext<DB>>,
1687{
1688    type IT = EthInterpreter;
1689
1690    fn inspect_run(
1691        &mut self,
1692        evm: &mut Self::Evm,
1693    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
1694        self.load_fee_fields(evm)?;
1695
1696        match self.inspect_run_without_catch_error(evm) {
1697            Ok(output) => Ok(output),
1698            Err(e) => self.catch_error(evm, e),
1699        }
1700    }
1701
1702    /// Overridden execution method with inspector support that handles AA vs standard transactions.
1703    ///
1704    /// Delegates to [`inspect_execution_with`](TempoEvmHandler::inspect_execution_with) with
1705    /// the default [`inspect_run_exec_loop`](Self::inspect_run_exec_loop).
1706    #[inline]
1707    fn inspect_execution(
1708        &mut self,
1709        evm: &mut Self::Evm,
1710        init_and_floor_gas: &InitialAndFloorGas,
1711    ) -> Result<FrameResult, Self::Error> {
1712        self.inspect_execution_with(evm, init_and_floor_gas, Self::inspect_run_exec_loop)
1713    }
1714}
1715
1716/// Helper function to create a frame result for an out of gas error.
1717///
1718/// Use native fn when new revm version is released.
1719#[inline]
1720fn oog_frame_result(kind: TxKind, gas_limit: u64) -> FrameResult {
1721    if kind.is_call() {
1722        FrameResult::new_call_oog(gas_limit, 0..0)
1723    } else {
1724        FrameResult::new_create_oog(gas_limit)
1725    }
1726}
1727
1728/// Checks if gas limit is sufficient and returns OOG frame result if not.
1729///
1730/// For T0+, validates gas limit covers intrinsic gas. For pre-T0, skips check
1731/// to maintain backward compatibility.
1732#[inline]
1733fn check_gas_limit(
1734    spec: tempo_chainspec::hardfork::TempoHardfork,
1735    tx: &TempoTxEnv,
1736    adjusted_gas: &InitialAndFloorGas,
1737) -> Option<FrameResult> {
1738    if spec.is_t0() && tx.gas_limit() < adjusted_gas.initial_gas {
1739        let kind = *tx
1740            .first_call()
1741            .expect("we already checked that there is at least one call in aa tx")
1742            .0;
1743        return Some(oog_frame_result(kind, tx.gas_limit()));
1744    }
1745    None
1746}
1747
1748/// Validates time window for AA transactions
1749///
1750/// AA transactions can have optional validBefore and validAfter fields:
1751/// - validAfter: Transaction can only be included after this timestamp
1752/// - validBefore: Transaction can only be included before this timestamp
1753///
1754/// This ensures transactions are only valid within a specific time window.
1755pub fn validate_time_window(
1756    valid_after: Option<u64>,
1757    valid_before: Option<u64>,
1758    block_timestamp: u64,
1759) -> Result<(), TempoInvalidTransaction> {
1760    // Validate validAfter constraint
1761    if let Some(after) = valid_after
1762        && block_timestamp < after
1763    {
1764        return Err(TempoInvalidTransaction::ValidAfter {
1765            current: block_timestamp,
1766            valid_after: after,
1767        });
1768    }
1769
1770    // Validate validBefore constraint
1771    // IMPORTANT: must be aligned with `fn has_expired_transactions` in `tempo-payload-builder`.
1772    if let Some(before) = valid_before
1773        && block_timestamp >= before
1774    {
1775        return Err(TempoInvalidTransaction::ValidBefore {
1776            current: block_timestamp,
1777            valid_before: before,
1778        });
1779    }
1780
1781    Ok(())
1782}
1783
1784#[cfg(test)]
1785mod tests {
1786    use super::*;
1787    use crate::{TempoBlockEnv, TempoTxEnv, evm::TempoEvm, tx::TempoBatchCallEnv};
1788    use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
1789    use proptest::prelude::*;
1790    use revm::{
1791        Context, Journal, MainContext,
1792        context::CfgEnv,
1793        database::{CacheDB, EmptyDB},
1794        handler::Handler,
1795        interpreter::{gas::COLD_ACCOUNT_ACCESS_COST, instructions::utility::IntoU256},
1796        primitives::hardfork::SpecId,
1797    };
1798    use tempo_chainspec::hardfork::TempoHardfork;
1799    use tempo_contracts::precompiles::DEFAULT_FEE_TOKEN;
1800    use tempo_precompiles::{PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS};
1801    use tempo_primitives::transaction::{
1802        Call, TempoSignature,
1803        tt_signature::{P256SignatureWithPreHash, WebAuthnSignature},
1804    };
1805
1806    fn create_test_journal() -> Journal<CacheDB<EmptyDB>> {
1807        let db = CacheDB::new(EmptyDB::default());
1808        Journal::new(db)
1809    }
1810
1811    #[test]
1812    fn test_invalid_fee_token_rejected() {
1813        // Test that an invalid fee token (non-TIP20 address) is rejected with InvalidFeeToken error
1814        // rather than panicking. This validates the check in load_fee_fields that guards against
1815        // invalid tokens reaching get_token_balance.
1816        let invalid_token = Address::random(); // Random address won't have TIP20 prefix
1817        assert!(
1818            !is_tip20_prefix(invalid_token),
1819            "Test requires a non-TIP20 address"
1820        );
1821
1822        let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
1823
1824        // Set up tx with the invalid token as fee_token
1825        let tx_env = TempoTxEnv {
1826            fee_token: Some(invalid_token),
1827            ..Default::default()
1828        };
1829
1830        let mut evm: TempoEvm<CacheDB<EmptyDB>, ()> = TempoEvm::new(
1831            Context::mainnet()
1832                .with_db(CacheDB::new(EmptyDB::default()))
1833                .with_block(TempoBlockEnv::default())
1834                .with_cfg(Default::default())
1835                .with_tx(tx_env),
1836            (),
1837        );
1838
1839        let result = handler.load_fee_fields(&mut evm);
1840
1841        assert!(
1842            matches!(
1843                result,
1844                Err(EVMError::Transaction(TempoInvalidTransaction::InvalidFeeToken(addr))) if addr == invalid_token
1845            ),
1846            "Should reject invalid fee token with InvalidFeeToken error"
1847        );
1848    }
1849
1850    #[test]
1851    fn test_self_sponsored_fee_payer_rejected_post_t2() {
1852        let caller = Address::random();
1853        let invalid_token = Address::random();
1854
1855        let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
1856        let mut cfg = CfgEnv::<TempoHardfork>::default();
1857        cfg.spec = TempoHardfork::T2;
1858
1859        let tx_env = TempoTxEnv {
1860            inner: revm::context::TxEnv {
1861                caller,
1862                ..Default::default()
1863            },
1864            fee_token: Some(invalid_token),
1865            fee_payer: Some(Some(caller)),
1866            ..Default::default()
1867        };
1868
1869        let mut evm: TempoEvm<CacheDB<EmptyDB>, ()> = TempoEvm::new(
1870            Context::mainnet()
1871                .with_db(CacheDB::new(EmptyDB::default()))
1872                .with_block(TempoBlockEnv::default())
1873                .with_cfg(cfg)
1874                .with_tx(tx_env),
1875            (),
1876        );
1877
1878        let result = handler.load_fee_fields(&mut evm);
1879        assert!(matches!(
1880            result,
1881            Err(EVMError::Transaction(
1882                TempoInvalidTransaction::SelfSponsoredFeePayer
1883            ))
1884        ));
1885    }
1886
1887    #[test]
1888    fn test_self_sponsored_fee_payer_not_rejected_pre_t2() {
1889        let caller = Address::random();
1890        let invalid_token = Address::random();
1891
1892        let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
1893        let mut cfg = CfgEnv::<TempoHardfork>::default();
1894        cfg.spec = TempoHardfork::T1C;
1895
1896        let tx_env = TempoTxEnv {
1897            inner: revm::context::TxEnv {
1898                caller,
1899                ..Default::default()
1900            },
1901            fee_token: Some(invalid_token),
1902            fee_payer: Some(Some(caller)),
1903            ..Default::default()
1904        };
1905
1906        let mut evm: TempoEvm<CacheDB<EmptyDB>, ()> = TempoEvm::new(
1907            Context::mainnet()
1908                .with_db(CacheDB::new(EmptyDB::default()))
1909                .with_block(TempoBlockEnv::default())
1910                .with_cfg(cfg)
1911                .with_tx(tx_env),
1912            (),
1913        );
1914
1915        let result = handler.load_fee_fields(&mut evm);
1916        assert!(matches!(
1917            result,
1918            Err(EVMError::Transaction(TempoInvalidTransaction::InvalidFeeToken(addr)))
1919                if addr == invalid_token
1920        ));
1921    }
1922
1923    #[test]
1924    fn test_get_token_balance() -> eyre::Result<()> {
1925        let mut journal = create_test_journal();
1926        // Use PATH_USD_ADDRESS which has the TIP20 prefix
1927        let token = PATH_USD_ADDRESS;
1928        let account = Address::random();
1929        let expected_balance = U256::random();
1930
1931        // Set up initial balance
1932        let balance_slot = TIP20Token::from_address(token)?.balances[account].slot();
1933        journal.load_account(token)?;
1934        journal
1935            .sstore(token, balance_slot, expected_balance)
1936            .unwrap();
1937
1938        let balance = get_token_balance(&mut journal, token, account)?;
1939        assert_eq!(balance, expected_balance);
1940
1941        Ok(())
1942    }
1943
1944    #[test]
1945    fn test_get_fee_token() -> eyre::Result<()> {
1946        let journal = create_test_journal();
1947        let mut ctx: TempoContext<_> = Context::mainnet()
1948            .with_db(CacheDB::new(EmptyDB::default()))
1949            .with_block(TempoBlockEnv::default())
1950            .with_cfg(Default::default())
1951            .with_tx(TempoTxEnv::default())
1952            .with_new_journal(journal);
1953        let user = Address::random();
1954        ctx.tx.inner.caller = user;
1955        let validator = Address::random();
1956        ctx.block.beneficiary = validator;
1957        let user_fee_token = Address::random();
1958        let validator_fee_token = Address::random();
1959        let tx_fee_token = Address::random();
1960
1961        // Set validator token
1962        let validator_slot = TipFeeManager::new().validator_tokens[validator].slot();
1963        ctx.journaled_state.load_account(TIP_FEE_MANAGER_ADDRESS)?;
1964        ctx.journaled_state
1965            .sstore(
1966                TIP_FEE_MANAGER_ADDRESS,
1967                validator_slot,
1968                validator_fee_token.into_u256(),
1969            )
1970            .unwrap();
1971
1972        {
1973            let fee_token = ctx
1974                .journaled_state
1975                .get_fee_token(&ctx.tx, user, ctx.cfg.spec)?;
1976            assert_eq!(DEFAULT_FEE_TOKEN, fee_token);
1977        }
1978
1979        // Set user token
1980        let user_slot = TipFeeManager::new().user_tokens[user].slot();
1981        ctx.journaled_state
1982            .sstore(
1983                TIP_FEE_MANAGER_ADDRESS,
1984                user_slot,
1985                user_fee_token.into_u256(),
1986            )
1987            .unwrap();
1988
1989        {
1990            let fee_token = ctx
1991                .journaled_state
1992                .get_fee_token(&ctx.tx, user, ctx.cfg.spec)?;
1993            assert_eq!(user_fee_token, fee_token);
1994        }
1995
1996        // Set tx fee token
1997        ctx.tx.fee_token = Some(tx_fee_token);
1998        let fee_token = ctx
1999            .journaled_state
2000            .get_fee_token(&ctx.tx, user, ctx.cfg.spec)?;
2001        assert_eq!(tx_fee_token, fee_token);
2002
2003        Ok(())
2004    }
2005
2006    #[test]
2007    fn test_aa_gas_single_call_vs_normal_tx() {
2008        use crate::TempoBatchCallEnv;
2009        use alloy_primitives::{Bytes, TxKind};
2010        use revm::interpreter::gas::calculate_initial_tx_gas;
2011        use tempo_primitives::transaction::{Call, TempoSignature};
2012        let gas_params = GasParams::default();
2013
2014        // Test that AA tx with secp256k1 and single call matches normal tx + per-call overhead
2015        let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); // 5 non-zero bytes
2016        let to = Address::random();
2017
2018        // Single call for AA
2019        let call = Call {
2020            to: TxKind::Call(to),
2021            value: U256::ZERO,
2022            input: calldata.clone(),
2023        };
2024
2025        let aa_env = TempoBatchCallEnv {
2026            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2027                alloy_primitives::Signature::test_signature(),
2028            )), // dummy secp256k1 sig
2029            aa_calls: vec![call],
2030            key_authorization: None,
2031            signature_hash: B256::ZERO,
2032            ..Default::default()
2033        };
2034
2035        // Calculate AA gas
2036        let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2037        let aa_gas = calculate_aa_batch_intrinsic_gas(
2038            &aa_env,
2039            &gas_params,
2040            None::<std::iter::Empty<&AccessListItem>>, // no access list
2041            spec,
2042        )
2043        .unwrap();
2044
2045        // Calculate expected gas using revm's function for equivalent normal tx
2046        let normal_tx_gas = calculate_initial_tx_gas(
2047            spec.into(),
2048            &calldata,
2049            false, // not create
2050            0,     // no access list accounts
2051            0,     // no access list storage
2052            0,     // no authorization list
2053        );
2054
2055        // AA with secp256k1 + single call should match normal tx exactly
2056        assert_eq!(aa_gas.initial_gas, normal_tx_gas.initial_gas);
2057    }
2058
2059    #[test]
2060    fn test_aa_gas_multiple_calls_overhead() {
2061        use crate::TempoBatchCallEnv;
2062        use alloy_primitives::{Bytes, TxKind};
2063        use revm::interpreter::gas::calculate_initial_tx_gas;
2064        use tempo_primitives::transaction::{Call, TempoSignature};
2065
2066        let calldata = Bytes::from(vec![1, 2, 3]); // 3 non-zero bytes
2067
2068        let calls = vec![
2069            Call {
2070                to: TxKind::Call(Address::random()),
2071                value: U256::ZERO,
2072                input: calldata.clone(),
2073            },
2074            Call {
2075                to: TxKind::Call(Address::random()),
2076                value: U256::ZERO,
2077                input: calldata.clone(),
2078            },
2079            Call {
2080                to: TxKind::Call(Address::random()),
2081                value: U256::ZERO,
2082                input: calldata.clone(),
2083            },
2084        ];
2085
2086        let aa_env = TempoBatchCallEnv {
2087            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2088                alloy_primitives::Signature::test_signature(),
2089            )),
2090            aa_calls: calls,
2091            key_authorization: None,
2092            signature_hash: B256::ZERO,
2093            ..Default::default()
2094        };
2095
2096        let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2097        let gas = calculate_aa_batch_intrinsic_gas(
2098            &aa_env,
2099            &GasParams::default(),
2100            None::<std::iter::Empty<&AccessListItem>>,
2101            spec,
2102        )
2103        .unwrap();
2104
2105        // Calculate base gas for a single normal tx
2106        let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
2107
2108        // For 3 calls: base (21k) + 3*calldata + 2*per-call overhead (calls 2 and 3)
2109        // = 21k + 2*(calldata cost) + 2*COLD_ACCOUNT_ACCESS_COST
2110        let expected = base_tx_gas.initial_gas
2111            + 2 * (calldata.len() as u64 * 16)
2112            + 2 * COLD_ACCOUNT_ACCESS_COST;
2113        // Should charge per-call overhead for calls beyond the first
2114        assert_eq!(gas.initial_gas, expected,);
2115    }
2116
2117    #[test]
2118    fn test_aa_gas_p256_signature() {
2119        use crate::TempoBatchCallEnv;
2120        use alloy_primitives::{B256, Bytes, TxKind};
2121        use revm::interpreter::gas::calculate_initial_tx_gas;
2122        use tempo_primitives::transaction::{
2123            Call, TempoSignature, tt_signature::P256SignatureWithPreHash,
2124        };
2125
2126        let spec = SpecId::CANCUN;
2127        let calldata = Bytes::from(vec![1, 2]);
2128
2129        let call = Call {
2130            to: TxKind::Call(Address::random()),
2131            value: U256::ZERO,
2132            input: calldata.clone(),
2133        };
2134
2135        let aa_env = TempoBatchCallEnv {
2136            signature: TempoSignature::Primitive(PrimitiveSignature::P256(
2137                P256SignatureWithPreHash {
2138                    r: B256::ZERO,
2139                    s: B256::ZERO,
2140                    pub_key_x: B256::ZERO,
2141                    pub_key_y: B256::ZERO,
2142                    pre_hash: false,
2143                },
2144            )),
2145            aa_calls: vec![call],
2146            key_authorization: None,
2147            signature_hash: B256::ZERO,
2148            ..Default::default()
2149        };
2150
2151        let gas = calculate_aa_batch_intrinsic_gas(
2152            &aa_env,
2153            &GasParams::default(),
2154            None::<std::iter::Empty<&AccessListItem>>,
2155            tempo_chainspec::hardfork::TempoHardfork::default(),
2156        )
2157        .unwrap();
2158
2159        // Calculate base gas for normal tx
2160        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
2161
2162        // Expected: normal tx + P256_VERIFY_GAS
2163        let expected = base_gas.initial_gas + P256_VERIFY_GAS;
2164        assert_eq!(gas.initial_gas, expected,);
2165    }
2166
2167    #[test]
2168    fn test_aa_gas_create_call() {
2169        use crate::TempoBatchCallEnv;
2170        use alloy_primitives::{Bytes, TxKind};
2171        use revm::interpreter::gas::calculate_initial_tx_gas;
2172        use tempo_primitives::transaction::{Call, TempoSignature};
2173
2174        let spec = SpecId::CANCUN; // Post-Shanghai
2175        let initcode = Bytes::from(vec![0x60, 0x80]); // 2 bytes
2176
2177        let call = Call {
2178            to: TxKind::Create,
2179            value: U256::ZERO,
2180            input: initcode.clone(),
2181        };
2182
2183        let aa_env = TempoBatchCallEnv {
2184            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2185                alloy_primitives::Signature::test_signature(),
2186            )),
2187            aa_calls: vec![call],
2188            key_authorization: None,
2189            signature_hash: B256::ZERO,
2190            ..Default::default()
2191        };
2192
2193        let gas = calculate_aa_batch_intrinsic_gas(
2194            &aa_env,
2195            &GasParams::default(),
2196            None::<std::iter::Empty<&AccessListItem>>,
2197            tempo_chainspec::hardfork::TempoHardfork::default(),
2198        )
2199        .unwrap();
2200
2201        // Calculate expected using revm's function for CREATE tx
2202        let base_gas = calculate_initial_tx_gas(
2203            spec, &initcode, true, // is_create = true
2204            0, 0, 0,
2205        );
2206
2207        // AA CREATE should match normal CREATE exactly
2208        assert_eq!(gas.initial_gas, base_gas.initial_gas,);
2209    }
2210
2211    #[test]
2212    fn test_aa_gas_value_transfer() {
2213        use crate::TempoBatchCallEnv;
2214        use alloy_primitives::{Bytes, TxKind};
2215        use tempo_primitives::transaction::{Call, TempoSignature};
2216
2217        let calldata = Bytes::from(vec![1]);
2218
2219        let call = Call {
2220            to: TxKind::Call(Address::random()),
2221            value: U256::from(1000), // Non-zero value
2222            input: calldata,
2223        };
2224
2225        let aa_env = TempoBatchCallEnv {
2226            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2227                alloy_primitives::Signature::test_signature(),
2228            )),
2229            aa_calls: vec![call],
2230            key_authorization: None,
2231            signature_hash: B256::ZERO,
2232            ..Default::default()
2233        };
2234
2235        let res = calculate_aa_batch_intrinsic_gas(
2236            &aa_env,
2237            &GasParams::default(),
2238            None::<std::iter::Empty<&AccessListItem>>,
2239            tempo_chainspec::hardfork::TempoHardfork::default(),
2240        );
2241
2242        assert_eq!(
2243            res.unwrap_err(),
2244            TempoInvalidTransaction::ValueTransferNotAllowedInAATx
2245        );
2246    }
2247
2248    #[test]
2249    fn test_aa_gas_access_list() {
2250        use crate::TempoBatchCallEnv;
2251        use alloy_primitives::{Bytes, TxKind};
2252        use revm::interpreter::gas::calculate_initial_tx_gas;
2253        use tempo_primitives::transaction::{Call, TempoSignature};
2254
2255        let spec = SpecId::CANCUN;
2256        let calldata = Bytes::from(vec![]);
2257
2258        let call = Call {
2259            to: TxKind::Call(Address::random()),
2260            value: U256::ZERO,
2261            input: calldata.clone(),
2262        };
2263
2264        let aa_env = TempoBatchCallEnv {
2265            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2266                alloy_primitives::Signature::test_signature(),
2267            )),
2268            aa_calls: vec![call],
2269            key_authorization: None,
2270            signature_hash: B256::ZERO,
2271            ..Default::default()
2272        };
2273
2274        // Test without access list
2275        let gas = calculate_aa_batch_intrinsic_gas(
2276            &aa_env,
2277            &GasParams::default(),
2278            None::<std::iter::Empty<&AccessListItem>>,
2279            tempo_chainspec::hardfork::TempoHardfork::default(),
2280        )
2281        .unwrap();
2282
2283        // Calculate expected using revm's function
2284        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
2285
2286        // Expected: normal tx
2287        assert_eq!(gas.initial_gas, base_gas.initial_gas,);
2288    }
2289
2290    #[test]
2291    fn test_key_authorization_rlp_encoding() {
2292        use alloy_primitives::{Address, U256};
2293        use tempo_primitives::transaction::{
2294            SignatureType, TokenLimit, key_authorization::KeyAuthorization,
2295        };
2296
2297        // Create test data
2298        let chain_id = 1u64;
2299        let key_type = SignatureType::Secp256k1;
2300        let key_id = Address::random();
2301        let expiry = 1000u64;
2302        let limits = vec![
2303            TokenLimit {
2304                token: Address::random(),
2305                limit: U256::from(100),
2306            },
2307            TokenLimit {
2308                token: Address::random(),
2309                limit: U256::from(200),
2310            },
2311        ];
2312
2313        // Compute hash using the helper function
2314        let hash1 = KeyAuthorization {
2315            chain_id,
2316            key_type,
2317            key_id,
2318            expiry: Some(expiry),
2319            limits: Some(limits.clone()),
2320        }
2321        .signature_hash();
2322
2323        // Compute again to verify consistency
2324        let hash2 = KeyAuthorization {
2325            chain_id,
2326            key_type,
2327            key_id,
2328            expiry: Some(expiry),
2329            limits: Some(limits.clone()),
2330        }
2331        .signature_hash();
2332
2333        assert_eq!(hash1, hash2, "Hash computation should be deterministic");
2334
2335        // Verify that different chain_id produces different hash
2336        let hash3 = KeyAuthorization {
2337            chain_id: 2,
2338            key_type,
2339            key_id,
2340            expiry: Some(expiry),
2341            limits: Some(limits),
2342        }
2343        .signature_hash();
2344        assert_ne!(
2345            hash1, hash3,
2346            "Different chain_id should produce different hash"
2347        );
2348    }
2349
2350    #[test]
2351    fn test_aa_gas_floor_gas_prague() {
2352        use crate::TempoBatchCallEnv;
2353        use alloy_primitives::{Bytes, TxKind};
2354        use revm::interpreter::gas::calculate_initial_tx_gas;
2355        use tempo_primitives::transaction::{Call, TempoSignature};
2356
2357        let spec = SpecId::PRAGUE;
2358        let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); // 5 non-zero bytes
2359
2360        let call = Call {
2361            to: TxKind::Call(Address::random()),
2362            value: U256::ZERO,
2363            input: calldata.clone(),
2364        };
2365
2366        let aa_env = TempoBatchCallEnv {
2367            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2368                alloy_primitives::Signature::test_signature(),
2369            )),
2370            aa_calls: vec![call],
2371            key_authorization: None,
2372            signature_hash: B256::ZERO,
2373            ..Default::default()
2374        };
2375
2376        let gas = calculate_aa_batch_intrinsic_gas(
2377            &aa_env,
2378            &GasParams::default(),
2379            None::<std::iter::Empty<&AccessListItem>>,
2380            tempo_chainspec::hardfork::TempoHardfork::default(),
2381        )
2382        .unwrap();
2383
2384        // Calculate expected floor gas using revm's function
2385        let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
2386
2387        // Floor gas should match revm's calculation for same calldata
2388        assert_eq!(
2389            gas.floor_gas, base_gas.floor_gas,
2390            "Should calculate floor gas for Prague matching revm"
2391        );
2392    }
2393
2394    /// This test will start failing once we get the balance transfer enabled
2395    /// PR that introduced [`TempoInvalidTransaction::ValueTransferNotAllowed`] https://github.com/tempoxyz/tempo/pull/759
2396    #[test]
2397    fn test_zero_value_transfer() -> eyre::Result<()> {
2398        use crate::TempoEvm;
2399
2400        // Create a test context with a transaction that has a non-zero value
2401        let ctx = Context::mainnet()
2402            .with_db(CacheDB::new(EmptyDB::default()))
2403            .with_block(Default::default())
2404            .with_cfg(Default::default())
2405            .with_tx(TempoTxEnv::default());
2406        let mut evm = TempoEvm::new(ctx, ());
2407
2408        // Set a non-zero value on the transaction
2409        evm.ctx.tx.inner.value = U256::from(1000);
2410
2411        // Create the handler
2412        let handler = TempoEvmHandler::<_, ()>::new();
2413
2414        // Call validate_env and expect it to fail with ValueTransferNotAllowed
2415        let result = handler.validate_env(&mut evm);
2416
2417        if let Err(EVMError::Transaction(err)) = result {
2418            assert_eq!(err, TempoInvalidTransaction::ValueTransferNotAllowed);
2419        } else {
2420            panic!("Expected ValueTransferNotAllowed error");
2421        }
2422
2423        Ok(())
2424    }
2425
2426    #[test]
2427    fn test_key_authorization_gas_with_limits() {
2428        use tempo_primitives::transaction::{
2429            KeyAuthorization, SignatureType, SignedKeyAuthorization, TokenLimit,
2430        };
2431
2432        // Helper to create key auth with N limits
2433        let create_key_auth = |num_limits: usize| -> SignedKeyAuthorization {
2434            let limits = if num_limits == 0 {
2435                None
2436            } else {
2437                Some(
2438                    (0..num_limits)
2439                        .map(|_| TokenLimit {
2440                            token: Address::random(),
2441                            limit: U256::from(1000),
2442                        })
2443                        .collect(),
2444                )
2445            };
2446
2447            SignedKeyAuthorization {
2448                authorization: KeyAuthorization {
2449                    chain_id: 1,
2450                    key_type: SignatureType::Secp256k1,
2451                    key_id: Address::random(),
2452                    expiry: None,
2453                    limits,
2454                },
2455                signature: PrimitiveSignature::Secp256k1(
2456                    alloy_primitives::Signature::test_signature(),
2457                ),
2458            }
2459        };
2460
2461        // Test 0 limits: base (27k) + ecrecover (3k) = 30,000
2462        let gas_0 = calculate_key_authorization_gas(
2463            &create_key_auth(0),
2464            &GasParams::default(),
2465            tempo_chainspec::hardfork::TempoHardfork::default(),
2466        );
2467        assert_eq!(
2468            gas_0,
2469            KEY_AUTH_BASE_GAS + ECRECOVER_GAS,
2470            "0 limits should be 30,000"
2471        );
2472
2473        // Test 1 limit: 30,000 + 22,000 = 52,000
2474        let gas_1 = calculate_key_authorization_gas(
2475            &create_key_auth(1),
2476            &GasParams::default(),
2477            tempo_chainspec::hardfork::TempoHardfork::default(),
2478        );
2479        assert_eq!(
2480            gas_1,
2481            KEY_AUTH_BASE_GAS + ECRECOVER_GAS + KEY_AUTH_PER_LIMIT_GAS,
2482            "1 limit should be 52,000"
2483        );
2484
2485        // Test 2 limits: 30,000 + 44,000 = 74,000
2486        let gas_2 = calculate_key_authorization_gas(
2487            &create_key_auth(2),
2488            &GasParams::default(),
2489            tempo_chainspec::hardfork::TempoHardfork::default(),
2490        );
2491        assert_eq!(
2492            gas_2,
2493            KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS,
2494            "2 limits should be 74,000"
2495        );
2496
2497        // Test 3 limits: 30,000 + 66,000 = 96,000
2498        let gas_3 = calculate_key_authorization_gas(
2499            &create_key_auth(3),
2500            &GasParams::default(),
2501            tempo_chainspec::hardfork::TempoHardfork::default(),
2502        );
2503        assert_eq!(
2504            gas_3,
2505            KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 3 * KEY_AUTH_PER_LIMIT_GAS,
2506            "3 limits should be 96,000"
2507        );
2508
2509        // T1B branch: gas = sig_gas + SLOAD + SSTORE * (1 + num_limits) + buffer
2510        let t1b_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T1B);
2511        let sstore =
2512            t1b_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
2513        let sload =
2514            t1b_gas_params.warm_storage_read_cost() + t1b_gas_params.cold_storage_additional_cost();
2515        const BUFFER: u64 = 2_000;
2516
2517        for num_limits in 0..=3 {
2518            let gas = calculate_key_authorization_gas(
2519                &create_key_auth(num_limits),
2520                &t1b_gas_params,
2521                TempoHardfork::T1B,
2522            );
2523            let expected = ECRECOVER_GAS + sload + sstore * (1 + num_limits as u64) + BUFFER;
2524            assert_eq!(gas, expected, "T1B with {num_limits} limits");
2525        }
2526    }
2527
2528    #[test]
2529    fn test_key_authorization_gas_in_batch() {
2530        use crate::TempoBatchCallEnv;
2531        use alloy_primitives::{Bytes, TxKind};
2532        use revm::interpreter::gas::calculate_initial_tx_gas;
2533        use tempo_primitives::transaction::{
2534            Call, KeyAuthorization, SignatureType, SignedKeyAuthorization, TempoSignature,
2535            TokenLimit,
2536        };
2537
2538        let calldata = Bytes::from(vec![1, 2, 3]);
2539
2540        let call = Call {
2541            to: TxKind::Call(Address::random()),
2542            value: U256::ZERO,
2543            input: calldata.clone(),
2544        };
2545
2546        // Create key authorization with 2 limits
2547        let key_auth: SignedKeyAuthorization = SignedKeyAuthorization {
2548            authorization: KeyAuthorization {
2549                chain_id: 1,
2550                key_type: SignatureType::Secp256k1,
2551                key_id: Address::random(),
2552                expiry: None,
2553                limits: Some(vec![
2554                    TokenLimit {
2555                        token: Address::random(),
2556                        limit: U256::from(1000),
2557                    },
2558                    TokenLimit {
2559                        token: Address::random(),
2560                        limit: U256::from(2000),
2561                    },
2562                ]),
2563            },
2564            signature: PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature()),
2565        };
2566
2567        let aa_env_with_key_auth = TempoBatchCallEnv {
2568            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2569                alloy_primitives::Signature::test_signature(),
2570            )),
2571            aa_calls: vec![call.clone()],
2572            key_authorization: Some(key_auth),
2573            signature_hash: B256::ZERO,
2574            ..Default::default()
2575        };
2576
2577        let aa_env_without_key_auth = TempoBatchCallEnv {
2578            signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2579                alloy_primitives::Signature::test_signature(),
2580            )),
2581            aa_calls: vec![call],
2582            key_authorization: None,
2583            signature_hash: B256::ZERO,
2584            ..Default::default()
2585        };
2586
2587        // Calculate gas WITH key authorization
2588        let gas_with_key_auth = calculate_aa_batch_intrinsic_gas(
2589            &aa_env_with_key_auth,
2590            &GasParams::default(),
2591            None::<std::iter::Empty<&AccessListItem>>,
2592            tempo_chainspec::hardfork::TempoHardfork::default(),
2593        )
2594        .unwrap();
2595
2596        // Calculate gas WITHOUT key authorization
2597        let gas_without_key_auth = calculate_aa_batch_intrinsic_gas(
2598            &aa_env_without_key_auth,
2599            &GasParams::default(),
2600            None::<std::iter::Empty<&AccessListItem>>,
2601            tempo_chainspec::hardfork::TempoHardfork::default(),
2602        )
2603        .unwrap();
2604
2605        // Expected key auth gas: 30,000 (base + ecrecover) + 2 * 22,000 (limits) = 74,000
2606        let expected_key_auth_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS;
2607
2608        assert_eq!(
2609            gas_with_key_auth.initial_gas - gas_without_key_auth.initial_gas,
2610            expected_key_auth_gas,
2611            "Key authorization should add exactly {expected_key_auth_gas} gas to batch",
2612        );
2613
2614        // Also verify absolute values
2615        let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2616        let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
2617        let expected_without = base_tx_gas.initial_gas; // no cold access for single call
2618        let expected_with = expected_without + expected_key_auth_gas;
2619
2620        assert_eq!(
2621            gas_without_key_auth.initial_gas, expected_without,
2622            "Gas without key auth should match expected"
2623        );
2624        assert_eq!(
2625            gas_with_key_auth.initial_gas, expected_with,
2626            "Gas with key auth should match expected"
2627        );
2628    }
2629
2630    #[test]
2631    fn test_2d_nonce_gas_in_intrinsic_gas() {
2632        use crate::gas_params::tempo_gas_params;
2633        use revm::{context_interface::cfg::GasId, handler::Handler};
2634
2635        const BASE_INTRINSIC_GAS: u64 = 21_000;
2636
2637        for spec in [
2638            TempoHardfork::Genesis,
2639            TempoHardfork::T0,
2640            TempoHardfork::T1,
2641            TempoHardfork::T1A,
2642            TempoHardfork::T1B,
2643            TempoHardfork::T2,
2644        ] {
2645            let gas_params = tempo_gas_params(spec);
2646
2647            let make_evm = |nonce: u64, nonce_key: U256| {
2648                let journal = Journal::new(CacheDB::new(EmptyDB::default()));
2649                let mut cfg = CfgEnv::<TempoHardfork>::default();
2650                cfg.spec = spec;
2651                cfg.gas_params = gas_params.clone();
2652                let ctx = Context::mainnet()
2653                    .with_db(CacheDB::new(EmptyDB::default()))
2654                    .with_block(TempoBlockEnv::default())
2655                    .with_cfg(cfg)
2656                    .with_tx(TempoTxEnv {
2657                        inner: revm::context::TxEnv {
2658                            gas_limit: 1_000_000,
2659                            nonce,
2660                            ..Default::default()
2661                        },
2662                        tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
2663                            aa_calls: vec![Call {
2664                                to: TxKind::Call(Address::random()),
2665                                value: U256::ZERO,
2666                                input: Bytes::new(),
2667                            }],
2668                            nonce_key,
2669                            ..Default::default()
2670                        })),
2671                        ..Default::default()
2672                    })
2673                    .with_new_journal(journal);
2674                TempoEvm::<_, ()>::new(ctx, ())
2675            };
2676
2677            let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
2678
2679            // Case 1: Protocol nonce (nonce_key == 0, nonce > 0) - no additional gas
2680            {
2681                let mut evm = make_evm(5, U256::ZERO);
2682                let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
2683                assert_eq!(
2684                    gas.initial_gas, BASE_INTRINSIC_GAS,
2685                    "{spec:?}: protocol nonce (nonce_key=0, nonce>0) should have no extra gas"
2686                );
2687            }
2688
2689            // Case 2: nonce_key != 0, nonce == 0
2690            {
2691                let expected = if spec.is_t1() {
2692                    // T1+: any nonce==0 charges new_account_cost (250k)
2693                    BASE_INTRINSIC_GAS + gas_params.get(GasId::new_account_cost())
2694                } else {
2695                    // Pre-T1: charges gas_new_nonce_key for new 2D key
2696                    BASE_INTRINSIC_GAS + spec.gas_new_nonce_key()
2697                };
2698                let mut evm = make_evm(0, U256::from(42));
2699                let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
2700                assert_eq!(
2701                    gas.initial_gas, expected,
2702                    "{spec:?}: nonce_key!=0, nonce==0 gas mismatch"
2703                );
2704            }
2705
2706            // Case 3: Existing 2D nonce key (nonce_key != 0, nonce > 0)
2707            {
2708                let mut evm = make_evm(5, U256::from(42));
2709                let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
2710                assert_eq!(
2711                    gas.initial_gas,
2712                    BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(),
2713                    "{spec:?}: existing 2D nonce key gas mismatch"
2714                );
2715            }
2716        }
2717    }
2718
2719    #[test]
2720    fn test_2d_nonce_gas_limit_validation() {
2721        use crate::gas_params::tempo_gas_params;
2722        use revm::{context_interface::cfg::GasId, handler::Handler};
2723
2724        const BASE_INTRINSIC_GAS: u64 = 21_000;
2725
2726        for spec in [
2727            TempoHardfork::Genesis,
2728            TempoHardfork::T0,
2729            TempoHardfork::T1,
2730            TempoHardfork::T2,
2731        ] {
2732            let gas_params = tempo_gas_params(spec);
2733
2734            // Build spec-specific test cases: (gas_limit, nonce, expected_result)
2735            let nonce_zero_gas = if spec.is_t1() {
2736                gas_params.get(GasId::new_account_cost())
2737            } else {
2738                spec.gas_new_nonce_key()
2739            };
2740
2741            let cases = if spec.is_t0() {
2742                vec![
2743                    (BASE_INTRINSIC_GAS + 10_000, 0u64, false), // Insufficient for nonce==0
2744                    (BASE_INTRINSIC_GAS + nonce_zero_gas, 0, true), // Exactly sufficient for nonce==0
2745                    (BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), // Exactly sufficient for existing key
2746                ]
2747            } else {
2748                // Genesis: nonce gas is added AFTER validation, so lower gas_limit still passes
2749                vec![
2750                    (BASE_INTRINSIC_GAS + 10_000, 0u64, true), // Passes validation (nonce gas added after)
2751                    (BASE_INTRINSIC_GAS + nonce_zero_gas, 0, true), // Also passes
2752                    (BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), // Also passes
2753                    (BASE_INTRINSIC_GAS - 1, 0, false),        // Below base intrinsic gas
2754                ]
2755            };
2756
2757            for (gas_limit, nonce, should_succeed) in cases {
2758                let journal = Journal::new(CacheDB::new(EmptyDB::default()));
2759                let mut cfg = CfgEnv::<TempoHardfork>::default();
2760                cfg.spec = spec;
2761                cfg.gas_params = gas_params.clone();
2762                let ctx = Context::mainnet()
2763                    .with_db(CacheDB::new(EmptyDB::default()))
2764                    .with_block(TempoBlockEnv::default())
2765                    .with_cfg(cfg)
2766                    .with_tx(TempoTxEnv {
2767                        inner: revm::context::TxEnv {
2768                            gas_limit,
2769                            nonce,
2770                            ..Default::default()
2771                        },
2772                        tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
2773                            aa_calls: vec![Call {
2774                                to: TxKind::Call(Address::random()),
2775                                value: U256::ZERO,
2776                                input: Bytes::new(),
2777                            }],
2778                            nonce_key: U256::from(1), // Non-zero to trigger 2D nonce gas
2779                            ..Default::default()
2780                        })),
2781                        ..Default::default()
2782                    })
2783                    .with_new_journal(journal);
2784
2785                let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
2786                let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
2787                let result = handler.validate_initial_tx_gas(&mut evm);
2788
2789                if should_succeed {
2790                    assert!(
2791                        result.is_ok(),
2792                        "{spec:?}: gas_limit={gas_limit}, nonce={nonce}: expected success but got error"
2793                    );
2794                } else {
2795                    let err = result.expect_err(&format!(
2796                        "{spec:?}: gas_limit={gas_limit}, nonce={nonce}: should fail"
2797                    ));
2798                    assert!(
2799                        matches!(
2800                            err.as_invalid_tx_err(),
2801                            Some(TempoInvalidTransaction::InsufficientGasForIntrinsicCost { .. })
2802                        ),
2803                        "Expected InsufficientGasForIntrinsicCost, got: {err:?}"
2804                    );
2805                }
2806            }
2807        }
2808    }
2809
2810    #[test]
2811    fn test_multicall_gas_refund_accounting() {
2812        use crate::evm::TempoEvm;
2813        use alloy_primitives::{Bytes, TxKind};
2814        use revm::{
2815            Context, Journal,
2816            context::CfgEnv,
2817            database::{CacheDB, EmptyDB},
2818            handler::FrameResult,
2819            interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult},
2820        };
2821        use tempo_primitives::transaction::Call;
2822
2823        const GAS_LIMIT: u64 = 1_000_000;
2824        const INTRINSIC_GAS: u64 = 21_000;
2825        // Mock call's gas: (CALL_0, CALL_1)
2826        const SPENT: (u64, u64) = (1000, 500);
2827        const REFUND: (i64, i64) = (100, 50);
2828
2829        // Create minimal EVM context
2830        let db = CacheDB::new(EmptyDB::default());
2831        let journal = Journal::new(db);
2832        let ctx = Context::mainnet()
2833            .with_db(CacheDB::new(EmptyDB::default()))
2834            .with_block(TempoBlockEnv::default())
2835            .with_cfg(CfgEnv::default())
2836            .with_tx(TempoTxEnv {
2837                inner: revm::context::TxEnv {
2838                    gas_limit: GAS_LIMIT,
2839                    ..Default::default()
2840                },
2841                ..Default::default()
2842            })
2843            .with_new_journal(journal);
2844
2845        let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
2846        let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
2847
2848        // Create mock calls
2849        let calls = vec![
2850            Call {
2851                to: TxKind::Call(Address::random()),
2852                value: U256::ZERO,
2853                input: Bytes::new(),
2854            },
2855            Call {
2856                to: TxKind::Call(Address::random()),
2857                value: U256::ZERO,
2858                input: Bytes::new(),
2859            },
2860        ];
2861
2862        let (mut call_idx, calls_gas) = (0, [(SPENT.0, REFUND.0), (SPENT.1, REFUND.1)]);
2863        let result = handler.execute_multi_call_with(
2864            &mut evm,
2865            &InitialAndFloorGas::new(INTRINSIC_GAS, 0),
2866            calls,
2867            |_handler, _evm, _gas| {
2868                let (spent, refund) = calls_gas[call_idx];
2869                call_idx += 1;
2870
2871                // Create gas with specific spent and refund values
2872                let mut gas = Gas::new(GAS_LIMIT);
2873                gas.set_spent(spent);
2874                gas.record_refund(refund);
2875
2876                // Mock successful frame result
2877                Ok(FrameResult::Call(CallOutcome::new(
2878                    InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas),
2879                    0..0,
2880                )))
2881            },
2882        );
2883
2884        let result = result.expect("execute_multi_call_with should succeed");
2885        let final_gas = result.gas();
2886
2887        assert_eq!(
2888            final_gas.spent(),
2889            INTRINSIC_GAS + SPENT.0 + SPENT.1,
2890            "Total spent should be intrinsic_gas + sum of all calls' spent values"
2891        );
2892        assert_eq!(
2893            final_gas.refunded(),
2894            REFUND.0 + REFUND.1,
2895            "Total refund should be sum of all calls' refunded values"
2896        );
2897        assert_eq!(
2898            final_gas.used(),
2899            INTRINSIC_GAS + SPENT.0 + SPENT.1 - (REFUND.0 + REFUND.1) as u64,
2900            "used() should be spent - refund"
2901        );
2902    }
2903
2904    /// Strategy for optional u64 timestamps.
2905    fn arb_opt_timestamp() -> impl Strategy<Value = Option<u64>> {
2906        prop_oneof![Just(None), any::<u64>().prop_map(Some)]
2907    }
2908
2909    /// Helper to create a secp256k1 signature for testing gas calculations.
2910    ///
2911    /// Note: We use a test signature rather than real valid/invalid signatures because
2912    /// these gas calculation functions only depend on the signature *type* (Secp256k1,
2913    /// P256, WebAuthn), not on cryptographic validity. Signature verification happens
2914    /// separately during `recover_signer()` before transactions enter the pool.
2915    fn secp256k1_sig() -> TempoSignature {
2916        TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2917            alloy_primitives::Signature::test_signature(),
2918        ))
2919    }
2920
2921    /// Helper to create a TempoBatchCallEnv with specified calls.
2922    fn make_aa_env(calls: Vec<Call>) -> TempoBatchCallEnv {
2923        TempoBatchCallEnv {
2924            signature: secp256k1_sig(),
2925            aa_calls: calls,
2926            key_authorization: None,
2927            signature_hash: B256::ZERO,
2928            ..Default::default()
2929        }
2930    }
2931
2932    /// Helper to create a single-call TempoBatchCallEnv with given calldata.
2933    fn make_single_call_env(calldata: Bytes) -> TempoBatchCallEnv {
2934        make_aa_env(vec![Call {
2935            to: TxKind::Call(Address::ZERO),
2936            value: U256::ZERO,
2937            input: calldata,
2938        }])
2939    }
2940
2941    /// Helper to create a multi-call TempoBatchCallEnv with N empty calls.
2942    fn make_multi_call_env(num_calls: usize) -> TempoBatchCallEnv {
2943        make_aa_env(
2944            (0..num_calls)
2945                .map(|_| Call {
2946                    to: TxKind::Call(Address::ZERO),
2947                    value: U256::ZERO,
2948                    input: Bytes::new(),
2949                })
2950                .collect(),
2951        )
2952    }
2953
2954    /// Helper to compute AA batch gas with no access list.
2955    fn compute_aa_gas(env: &TempoBatchCallEnv) -> InitialAndFloorGas {
2956        calculate_aa_batch_intrinsic_gas(
2957            env,
2958            &GasParams::default(),
2959            None::<std::iter::Empty<&AccessListItem>>,
2960            tempo_chainspec::hardfork::TempoHardfork::default(),
2961        )
2962        .unwrap()
2963    }
2964
2965    proptest! {
2966        #![proptest_config(ProptestConfig::with_cases(500))]
2967
2968        /// Property: validate_time_window returns Ok if (after <= ts < before)
2969        #[test]
2970        fn proptest_validate_time_window_correctness(
2971            valid_after in arb_opt_timestamp(),
2972            valid_before in arb_opt_timestamp(),
2973            block_timestamp in any::<u64>(),
2974        ) {
2975            let result = validate_time_window(valid_after, valid_before, block_timestamp);
2976
2977            let after_ok = valid_after.is_none_or(|after| block_timestamp >= after);
2978            let before_ok = valid_before.is_none_or(|before| block_timestamp < before);
2979            let expected_valid = after_ok && before_ok;
2980
2981            prop_assert_eq!(result.is_ok(), expected_valid,
2982                "valid_after={:?}, valid_before={:?}, block_ts={}, result={:?}",
2983                valid_after, valid_before, block_timestamp, result);
2984        }
2985
2986        /// Property: validate_time_window with None constraints always succeeds
2987        #[test]
2988        fn proptest_validate_time_window_none_always_valid(block_timestamp in any::<u64>()) {
2989            prop_assert!(validate_time_window(None, None, block_timestamp).is_ok());
2990        }
2991
2992        /// Property: validate_time_window with valid_after=0 is equivalent to None
2993        ///
2994        /// This tests the equivalence property: Some(0) and None for valid_after should produce
2995        /// identical results regardless of what valid_before is. We intentionally don't constrain
2996        /// valid_before because we're testing that the equivalence holds in all cases (both when
2997        /// valid_before causes success and when it causes failure).
2998        #[test]
2999        fn proptest_validate_time_window_zero_after_equivalent_to_none(
3000            valid_before in arb_opt_timestamp(),
3001            block_timestamp in any::<u64>(),
3002        ) {
3003            let with_zero = validate_time_window(Some(0), valid_before, block_timestamp);
3004            let with_none = validate_time_window(None, valid_before, block_timestamp);
3005            prop_assert_eq!(with_zero.is_ok(), with_none.is_ok());
3006        }
3007
3008        /// Property: validate_time_window - if before <= after, the window is empty
3009        #[test]
3010        fn proptest_validate_time_window_empty_window(
3011            valid_after in 1u64..=u64::MAX,
3012            offset in 0u64..1000u64,
3013        ) {
3014            let valid_before = valid_after.saturating_sub(offset);
3015            let result = validate_time_window(Some(valid_after), Some(valid_before), valid_after);
3016            prop_assert!(result.is_err(), "Empty window should reject all timestamps");
3017        }
3018
3019        /// Property: signature gas ordering is consistent: secp256k1 <= p256 <= webauthn
3020        #[test]
3021        fn proptest_signature_gas_ordering(webauthn_data_len in 0usize..1000) {
3022            let secp_sig = PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature());
3023            let p256_sig = PrimitiveSignature::P256(P256SignatureWithPreHash {
3024                r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO, pre_hash: false,
3025            });
3026            let webauthn_sig = PrimitiveSignature::WebAuthn(WebAuthnSignature {
3027                r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO,
3028                webauthn_data: Bytes::from(vec![0u8; webauthn_data_len]),
3029            });
3030
3031            let secp_gas = primitive_signature_verification_gas(&secp_sig);
3032            let p256_gas = primitive_signature_verification_gas(&p256_sig);
3033            let webauthn_gas = primitive_signature_verification_gas(&webauthn_sig);
3034
3035            prop_assert!(secp_gas <= p256_gas, "secp256k1 should be <= p256");
3036            prop_assert!(p256_gas <= webauthn_gas, "p256 should be <= webauthn");
3037        }
3038
3039        /// Property: gas calculation monotonicity - more calldata means more gas (non-zero bytes)
3040        /// Non-zero bytes cost 16 gas each, so monotonicity holds for uniform non-zero calldata.
3041        #[test]
3042        fn proptest_gas_monotonicity_calldata_nonzero(
3043            calldata_len1 in 0usize..1000,
3044            calldata_len2 in 0usize..1000,
3045        ) {
3046            let gas1 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len1])));
3047            let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len2])));
3048
3049            if calldata_len1 <= calldata_len2 {
3050                prop_assert!(gas1.initial_gas <= gas2.initial_gas,
3051                    "More calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}",
3052                    calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas);
3053            } else {
3054                prop_assert!(gas1.initial_gas >= gas2.initial_gas,
3055                    "Less calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}",
3056                    calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas);
3057            }
3058        }
3059
3060        /// Property: gas calculation monotonicity - more calldata means more gas (zero bytes)
3061        /// Zero bytes cost 4 gas each, so monotonicity holds for uniform zero calldata.
3062        #[test]
3063        fn proptest_gas_monotonicity_calldata_zero(
3064            calldata_len1 in 0usize..1000,
3065            calldata_len2 in 0usize..1000,
3066        ) {
3067            let gas1 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len1])));
3068            let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len2])));
3069
3070            if calldata_len1 <= calldata_len2 {
3071                prop_assert!(gas1.initial_gas <= gas2.initial_gas,
3072                    "More zero-byte calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}",
3073                    calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas);
3074            } else {
3075                prop_assert!(gas1.initial_gas >= gas2.initial_gas,
3076                    "Less zero-byte calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}",
3077                    calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas);
3078            }
3079        }
3080
3081        /// Property: zero-byte calldata costs less gas than non-zero byte calldata of same length.
3082        /// Zero bytes cost 4 gas each, non-zero bytes cost 16 gas each.
3083        #[test]
3084        fn proptest_zero_bytes_cheaper_than_nonzero(calldata_len in 1usize..1000) {
3085            let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len])));
3086            let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len])));
3087
3088            prop_assert!(zero_gas.initial_gas < nonzero_gas.initial_gas,
3089                "Zero-byte calldata should cost less: len={}, zero_gas={}, nonzero_gas={}",
3090                calldata_len, zero_gas.initial_gas, nonzero_gas.initial_gas);
3091        }
3092
3093        /// Property: mixed calldata gas is bounded by all-zero and all-nonzero extremes.
3094        /// Gas for mixed calldata should be between gas for all-zero and all-nonzero of same length.
3095        #[test]
3096        fn proptest_mixed_calldata_gas_bounded(
3097            calldata_len in 1usize..500,
3098            nonzero_ratio in 0u8..=100,
3099        ) {
3100            // Create mixed calldata where nonzero_ratio% of bytes are non-zero
3101            let calldata: Vec<u8> = (0..calldata_len)
3102                .map(|i| if (i * 100 / calldata_len) < nonzero_ratio as usize { 1u8 } else { 0u8 })
3103                .collect();
3104
3105            let mixed_gas = compute_aa_gas(&make_single_call_env(Bytes::from(calldata)));
3106            let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len])));
3107            let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len])));
3108
3109            prop_assert!(mixed_gas.initial_gas >= zero_gas.initial_gas,
3110                "Mixed calldata gas should be >= all-zero gas: mixed={}, zero={}",
3111                mixed_gas.initial_gas, zero_gas.initial_gas);
3112            prop_assert!(mixed_gas.initial_gas <= nonzero_gas.initial_gas,
3113                "Mixed calldata gas should be <= all-nonzero gas: mixed={}, nonzero={}",
3114                mixed_gas.initial_gas, nonzero_gas.initial_gas);
3115        }
3116
3117        /// Property: gas calculation monotonicity - more calls means more gas
3118        #[test]
3119        fn proptest_gas_monotonicity_call_count(
3120            num_calls1 in 1usize..10,
3121            num_calls2 in 1usize..10,
3122        ) {
3123            let gas1 = compute_aa_gas(&make_multi_call_env(num_calls1));
3124            let gas2 = compute_aa_gas(&make_multi_call_env(num_calls2));
3125
3126            if num_calls1 <= num_calls2 {
3127                prop_assert!(gas1.initial_gas <= gas2.initial_gas,
3128                    "More calls should mean more gas: calls1={}, gas1={}, calls2={}, gas2={}",
3129                    num_calls1, gas1.initial_gas, num_calls2, gas2.initial_gas);
3130            } else {
3131                prop_assert!(gas1.initial_gas >= gas2.initial_gas,
3132                    "Fewer calls should mean less gas: calls1={}, gas1={}, calls2={}, gas2={}",
3133                    num_calls1, gas1.initial_gas, num_calls2, gas2.initial_gas);
3134            }
3135        }
3136
3137        /// Property: AA batch gas with Secp256k1 signature equals exactly 21k base + cold access
3138        ///
3139        /// For minimal AA transactions (Secp256k1 sig, no calldata, no access list):
3140        /// - Base: 21,000 (same base stipend as regular transactions)
3141        /// - Plus: COLD_ACCOUNT_ACCESS_COST per additional call beyond the first
3142        ///
3143        /// AA transactions use the same 21k base as regular transactions because
3144        /// Secp256k1 signature verification adds 0 extra gas. Other signature types
3145        /// (P256, WebAuthn) add 5,000+ gas beyond this base.
3146        #[test]
3147        fn proptest_gas_aa_secp256k1_exact_bounds(num_calls in 1usize..5) {
3148            let gas = compute_aa_gas(&make_multi_call_env(num_calls));
3149
3150            // Expected exactly: 21k base + cold account access for each additional call
3151            let expected = 21_000 + COLD_ACCOUNT_ACCESS_COST * (num_calls.saturating_sub(1) as u64);
3152            prop_assert_eq!(gas.initial_gas, expected,
3153                "Gas {} should equal expected {} for {} calls (21k + {}*COLD_ACCOUNT_ACCESS_COST)",
3154                gas.initial_gas, expected, num_calls, num_calls.saturating_sub(1));
3155        }
3156
3157        /// Property: first_call returns the first call for AA transactions with any number of calls
3158        #[test]
3159        fn proptest_first_call_returns_first_for_aa(num_calls in 1usize..10) {
3160            let calls: Vec<Call> = (0..num_calls)
3161                .map(|i| Call {
3162                    to: TxKind::Call(Address::with_last_byte(i as u8)),
3163                    value: U256::ZERO,
3164                    input: Bytes::from(vec![i as u8; i + 1]),
3165                })
3166                .collect();
3167
3168            let expected_addr = Address::with_last_byte(0);
3169            let expected_input = vec![0u8; 1];
3170
3171            let tx_env = TempoTxEnv {
3172                inner: revm::context::TxEnv::default(),
3173                tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3174                    aa_calls: calls,
3175                    signature: secp256k1_sig(),
3176                    signature_hash: B256::ZERO,
3177                    ..Default::default()
3178                })),
3179                ..Default::default()
3180            };
3181
3182            let first = tx_env.first_call();
3183            prop_assert!(first.is_some(), "first_call should return Some for non-empty AA calls");
3184
3185            let (kind, input) = first.unwrap();
3186            prop_assert_eq!(*kind, TxKind::Call(expected_addr), "Should return first call's address");
3187            prop_assert_eq!(input, expected_input.as_slice(), "Should return first call's input");
3188        }
3189
3190        /// Property: first_call returns None for AA transaction with zero calls
3191        #[test]
3192        fn proptest_first_call_empty_aa(_dummy in 0u8..1) {
3193            let tx_env = TempoTxEnv {
3194                inner: revm::context::TxEnv::default(),
3195                tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3196                    aa_calls: vec![],
3197                    signature: secp256k1_sig(),
3198                    signature_hash: B256::ZERO,
3199                    ..Default::default()
3200                })),
3201                ..Default::default()
3202            };
3203
3204            prop_assert!(tx_env.first_call().is_none(), "first_call should return None for empty AA calls");
3205        }
3206
3207        /// Property: first_call returns inner tx data for non-AA transactions
3208        #[test]
3209        fn proptest_first_call_non_aa(calldata_len in 0usize..100) {
3210            let calldata = Bytes::from(vec![0xab_u8; calldata_len]);
3211            let target = Address::random();
3212
3213            let tx_env = TempoTxEnv {
3214                inner: revm::context::TxEnv {
3215                    kind: TxKind::Call(target),
3216                    data: calldata.clone(),
3217                    ..Default::default()
3218                },
3219                tempo_tx_env: None,
3220                ..Default::default()
3221            };
3222
3223            let first = tx_env.first_call();
3224            prop_assert!(first.is_some(), "first_call should return Some for non-AA tx");
3225
3226            let (kind, input) = first.unwrap();
3227            prop_assert_eq!(*kind, TxKind::Call(target), "Should return inner tx kind");
3228            prop_assert_eq!(input, calldata.as_ref(), "Should return inner tx data");
3229        }
3230
3231        /// Property: calculate_key_authorization_gas is monotonic in number of limits
3232        #[test]
3233        fn proptest_key_auth_gas_monotonic_limits(
3234            num_limits1 in 0usize..10,
3235            num_limits2 in 0usize..10,
3236        ) {
3237            use tempo_primitives::transaction::{
3238                SignatureType, SignedKeyAuthorization,
3239                key_authorization::KeyAuthorization,
3240                TokenLimit as PrimTokenLimit,
3241            };
3242
3243            let make_key_auth = |num_limits: usize| -> SignedKeyAuthorization {
3244                let limits = if num_limits == 0 {
3245                    None
3246                } else {
3247                    Some((0..num_limits).map(|i| PrimTokenLimit {
3248                        token: Address::with_last_byte(i as u8),
3249                        limit: U256::from(1000),
3250                    }).collect())
3251                };
3252
3253                SignedKeyAuthorization {
3254                    authorization: KeyAuthorization {
3255                        chain_id: 1,
3256                        key_type: SignatureType::Secp256k1,
3257                        key_id: Address::ZERO,
3258                        expiry: None,
3259                        limits,
3260                    },
3261                    signature: PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature()),
3262                }
3263            };
3264
3265            // Test both pre-T1B and T1B branches
3266            for (gas_params, spec) in [
3267                (GasParams::default(), tempo_chainspec::hardfork::TempoHardfork::default()),
3268                (crate::gas_params::tempo_gas_params(TempoHardfork::T1B), TempoHardfork::T1B),
3269            ] {
3270                let gas1 = calculate_key_authorization_gas(&make_key_auth(num_limits1), &gas_params, spec);
3271                let gas2 = calculate_key_authorization_gas(&make_key_auth(num_limits2), &gas_params, spec);
3272
3273                if num_limits1 <= num_limits2 {
3274                    prop_assert!(gas1 <= gas2,
3275                        "{spec:?}: More limits should mean more gas: limits1={}, gas1={}, limits2={}, gas2={}",
3276                        num_limits1, gas1, num_limits2, gas2);
3277                } else {
3278                    prop_assert!(gas1 >= gas2,
3279                        "{spec:?}: Fewer limits should mean less gas: limits1={}, gas1={}, limits2={}, gas2={}",
3280                        num_limits1, gas1, num_limits2, gas2);
3281                }
3282            }
3283        }
3284
3285        /// Property: calculate_key_authorization_gas minimum is KEY_AUTH_BASE_GAS + ECRECOVER_GAS
3286        #[test]
3287        fn proptest_key_auth_gas_minimum(
3288            sig_type in 0u8..3,
3289            num_limits in 0usize..5,
3290        ) {
3291            use tempo_primitives::transaction::{
3292                SignatureType, SignedKeyAuthorization,
3293                key_authorization::KeyAuthorization,
3294                TokenLimit as PrimTokenLimit,
3295            };
3296
3297            let signature = match sig_type {
3298                0 => PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature()),
3299                1 => PrimitiveSignature::P256(P256SignatureWithPreHash {
3300                    r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO, pre_hash: false,
3301                }),
3302                _ => PrimitiveSignature::WebAuthn(WebAuthnSignature {
3303                    r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO,
3304                    webauthn_data: Bytes::new(),
3305                }),
3306            };
3307
3308            let key_auth = SignedKeyAuthorization {
3309                authorization: KeyAuthorization {
3310                    chain_id: 1,
3311                    key_type: SignatureType::Secp256k1,
3312                    key_id: Address::ZERO,
3313                    expiry: None,
3314                    limits: if num_limits == 0 { None } else {
3315                        Some((0..num_limits).map(|i| PrimTokenLimit {
3316                            token: Address::with_last_byte(i as u8),
3317                            limit: U256::from(1000),
3318                        }).collect())
3319                    },
3320                },
3321                signature,
3322            };
3323
3324            // Pre-T1B: minimum is KEY_AUTH_BASE_GAS + ECRECOVER_GAS
3325            let gas = calculate_key_authorization_gas(&key_auth, &GasParams::default(), tempo_chainspec::hardfork::TempoHardfork::default());
3326            let min_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS;
3327            prop_assert!(gas >= min_gas,
3328                "Pre-T1B: Key auth gas should be at least {min_gas}, got {gas}");
3329
3330            // T1B: minimum is ECRECOVER_GAS + sload + sstore (0 limits)
3331            let t1b_params = crate::gas_params::tempo_gas_params(TempoHardfork::T1B);
3332            let gas_t1b = calculate_key_authorization_gas(&key_auth, &t1b_params, TempoHardfork::T1B);
3333            let sstore = t1b_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
3334            let sload = t1b_params.warm_storage_read_cost() + t1b_params.cold_storage_additional_cost();
3335            let min_t1b = ECRECOVER_GAS + sload + sstore;
3336            prop_assert!(gas_t1b >= min_t1b,
3337                "T1B: Key auth gas should be at least {min_t1b}, got {gas_t1b}");
3338        }
3339    }
3340
3341    /// Test that T1 hardfork correctly charges 250k gas for nonce == 0.
3342    ///
3343    /// This test validates [TIP-1000]'s requirement:
3344    /// "Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas"
3345    ///
3346    /// The test proves the audit finding (claiming only 22,100 gas is charged) is a false positive
3347    /// by using delta-based assertions: gas(nonce=0) - gas(nonce>0) == new_account_cost.
3348    ///
3349    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
3350    #[test]
3351    fn test_t1_2d_nonce_key_charges_250k_gas() {
3352        use crate::gas_params::tempo_gas_params;
3353        use revm::{context_interface::cfg::GasId, handler::Handler};
3354
3355        // Deterministic test addresses
3356        const TEST_TARGET: Address = Address::new([0xAA; 20]);
3357        const TEST_NONCE_KEY: U256 = U256::from_limbs([42, 0, 0, 0]);
3358        const SPEC: TempoHardfork = TempoHardfork::T1;
3359        const NEW_NONCE_KEY_GAS: u64 = SPEC.gas_new_nonce_key();
3360        const EXISTING_NONCE_KEY_GAS: u64 = SPEC.gas_existing_nonce_key();
3361
3362        // Create T1 config with TIP-1000 gas params
3363        let mut cfg = CfgEnv::<TempoHardfork>::default();
3364        cfg.spec = SPEC;
3365        cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
3366
3367        // Get the expected new_account_cost dynamically from gas params
3368        let new_account_cost = cfg.gas_params.get(GasId::new_account_cost());
3369        assert_eq!(
3370            new_account_cost, 250_000,
3371            "T1 gas params should have 250k new_account_cost"
3372        );
3373
3374        // Helper to create EVM context for testing
3375        let make_evm = |cfg: CfgEnv<TempoHardfork>, nonce: u64, nonce_key: U256| {
3376            let journal = Journal::new(CacheDB::new(EmptyDB::default()));
3377            let ctx = Context::mainnet()
3378                .with_db(CacheDB::new(EmptyDB::default()))
3379                .with_block(TempoBlockEnv::default())
3380                .with_cfg(cfg)
3381                .with_tx(TempoTxEnv {
3382                    inner: revm::context::TxEnv {
3383                        gas_limit: 1_000_000,
3384                        nonce,
3385                        ..Default::default()
3386                    },
3387                    tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3388                        aa_calls: vec![Call {
3389                            to: TxKind::Call(TEST_TARGET),
3390                            value: U256::ZERO,
3391                            input: Bytes::new(),
3392                        }],
3393                        nonce_key,
3394                        ..Default::default()
3395                    })),
3396                    ..Default::default()
3397                })
3398                .with_new_journal(journal);
3399            TempoEvm::<_, ()>::new(ctx, ())
3400        };
3401
3402        // Case 1: nonce == 0 with 2D nonce key -> should include new_account_cost
3403        let mut evm_nonce_zero = make_evm(cfg.clone(), 0, TEST_NONCE_KEY);
3404        let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
3405        let gas_nonce_zero = handler
3406            .validate_initial_tx_gas(&mut evm_nonce_zero)
3407            .unwrap();
3408
3409        // Case 2: nonce > 0 with same 2D nonce key -> should charge EXISTING_NONCE_KEY_GAS (5k)
3410        // This tests that existing 2D nonce keys are charged 5k gas per TIP-1000 Invariant 3
3411        let mut evm_nonce_five = make_evm(cfg.clone(), 5, TEST_NONCE_KEY);
3412        let gas_nonce_five = handler
3413            .validate_initial_tx_gas(&mut evm_nonce_five)
3414            .unwrap();
3415
3416        // Delta-based assertion: the difference should be new_account_cost - EXISTING_NONCE_KEY_GAS
3417        // nonce=0 charges 250k (new account), nonce>0 charges 5k (existing key update)
3418        let gas_delta = gas_nonce_zero.initial_gas - gas_nonce_five.initial_gas;
3419        let expected_delta = new_account_cost - EXISTING_NONCE_KEY_GAS;
3420        assert_eq!(
3421            gas_delta, expected_delta,
3422            "T1 gas difference between nonce=0 and nonce>0 should be {expected_delta} (new_account_cost - EXISTING_NONCE_KEY_GAS), got {gas_delta}"
3423        );
3424
3425        // Verify it's NOT using the pre-T1 NEW_NONCE_KEY_GAS (22,100)
3426        assert_ne!(
3427            gas_delta, NEW_NONCE_KEY_GAS,
3428            "T1 should NOT use pre-T1 NEW_NONCE_KEY_GAS ({NEW_NONCE_KEY_GAS}) for nonce=0 transactions"
3429        );
3430
3431        // Case 3: nonce == 0 with regular nonce (nonce_key=0) -> same +250k charge
3432        let mut evm_regular_nonce = make_evm(cfg, 0, U256::ZERO);
3433        let gas_regular = handler
3434            .validate_initial_tx_gas(&mut evm_regular_nonce)
3435            .unwrap();
3436
3437        assert_eq!(
3438            gas_nonce_zero.initial_gas, gas_regular.initial_gas,
3439            "nonce=0 should charge the same regardless of nonce_key (2D vs regular)"
3440        );
3441    }
3442
3443    /// Test that T1 hardfork correctly charges 5k gas for existing 2D nonce keys (nonce > 0).
3444    ///
3445    /// This test validates [TIP-1000] Invariant 3:
3446    /// "SSTORE operations that modify existing non-zero state (non-zero to non-zero)
3447    /// MUST continue to charge 5,000 gas"
3448    ///
3449    /// When using an existing 2D nonce key (nonce_key != 0 && nonce > 0), the nonce value
3450    /// transitions from N to N+1 (non-zero to non-zero), which must charge EXISTING_NONCE_KEY_GAS.
3451    ///
3452    /// [TIP-1000]: <https://docs.tempo.xyz/protocol/tips/tip-1000>
3453    #[test]
3454    fn test_t1_existing_2d_nonce_key_charges_5k_gas() {
3455        use crate::gas_params::tempo_gas_params;
3456        use revm::handler::Handler;
3457
3458        const BASE_INTRINSIC_GAS: u64 = 21_000;
3459        const TEST_TARGET: Address = Address::new([0xBB; 20]);
3460        const TEST_NONCE_KEY: U256 = U256::from_limbs([99, 0, 0, 0]);
3461        const SPEC: TempoHardfork = TempoHardfork::T1;
3462        const EXISTING_NONCE_KEY_GAS: u64 = SPEC.gas_existing_nonce_key();
3463
3464        let mut cfg = CfgEnv::<TempoHardfork>::default();
3465        cfg.spec = SPEC;
3466        cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
3467
3468        let make_evm = |cfg: CfgEnv<TempoHardfork>, nonce: u64, nonce_key: U256| {
3469            let journal = Journal::new(CacheDB::new(EmptyDB::default()));
3470            let ctx = Context::mainnet()
3471                .with_db(CacheDB::new(EmptyDB::default()))
3472                .with_block(TempoBlockEnv::default())
3473                .with_cfg(cfg)
3474                .with_tx(TempoTxEnv {
3475                    inner: revm::context::TxEnv {
3476                        gas_limit: 1_000_000,
3477                        nonce,
3478                        ..Default::default()
3479                    },
3480                    tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3481                        aa_calls: vec![Call {
3482                            to: TxKind::Call(TEST_TARGET),
3483                            value: U256::ZERO,
3484                            input: Bytes::new(),
3485                        }],
3486                        nonce_key,
3487                        ..Default::default()
3488                    })),
3489                    ..Default::default()
3490                })
3491                .with_new_journal(journal);
3492            TempoEvm::<_, ()>::new(ctx, ())
3493        };
3494
3495        let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
3496
3497        // Case 1: Existing 2D nonce key (nonce > 0) should charge EXISTING_NONCE_KEY_GAS
3498        let mut evm_existing_key = make_evm(cfg.clone(), 5, TEST_NONCE_KEY);
3499        let gas_existing = handler
3500            .validate_initial_tx_gas(&mut evm_existing_key)
3501            .unwrap();
3502
3503        assert_eq!(
3504            gas_existing.initial_gas,
3505            BASE_INTRINSIC_GAS + EXISTING_NONCE_KEY_GAS,
3506            "T1 existing 2D nonce key (nonce>0) should charge BASE + EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})"
3507        );
3508
3509        // Case 2: Regular nonce (nonce_key = 0) with nonce > 0 should NOT charge extra gas
3510        let mut evm_regular = make_evm(cfg, 5, U256::ZERO);
3511        let gas_regular = handler.validate_initial_tx_gas(&mut evm_regular).unwrap();
3512
3513        assert_eq!(
3514            gas_regular.initial_gas, BASE_INTRINSIC_GAS,
3515            "T1 regular nonce (nonce_key=0, nonce>0) should only charge BASE intrinsic gas"
3516        );
3517
3518        // Verify the delta between 2D and regular nonce is exactly EXISTING_NONCE_KEY_GAS
3519        let gas_delta = gas_existing.initial_gas - gas_regular.initial_gas;
3520        assert_eq!(
3521            gas_delta, EXISTING_NONCE_KEY_GAS,
3522            "Difference between existing 2D nonce and regular nonce should be EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})"
3523        );
3524    }
3525}